summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/mod.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/math/mod.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/math/mod.rs')
-rw-r--r--crates/typst-library/src/math/mod.rs480
1 files changed, 480 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs
new file mode 100644
index 00000000..0429265f
--- /dev/null
+++ b/crates/typst-library/src/math/mod.rs
@@ -0,0 +1,480 @@
+//! Mathematical formulas.
+
+#[macro_use]
+mod ctx;
+mod accent;
+mod align;
+mod attach;
+mod cancel;
+mod delimited;
+mod frac;
+mod fragment;
+mod matrix;
+mod op;
+mod root;
+mod row;
+mod spacing;
+mod stretch;
+mod style;
+mod underover;
+
+pub use self::accent::*;
+pub use self::align::*;
+pub use self::attach::*;
+pub use self::cancel::*;
+pub use self::delimited::*;
+pub use self::frac::*;
+pub use self::matrix::*;
+pub use self::op::*;
+pub use self::root::*;
+pub use self::style::*;
+pub use self::underover::*;
+
+use ttf_parser::{GlyphId, Rect};
+use typst::eval::{Module, Scope};
+use typst::font::{Font, FontWeight};
+use typst::model::Guard;
+use typst::util::option_eq;
+use unicode_math_class::MathClass;
+
+use self::ctx::*;
+use self::fragment::*;
+use self::row::*;
+use self::spacing::*;
+use crate::layout::{HElem, ParElem, Spacing};
+use crate::meta::Supplement;
+use crate::meta::{
+ Count, Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable,
+};
+use crate::prelude::*;
+use crate::text::{
+ families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize,
+};
+
+/// Create a module with all math definitions.
+pub fn module() -> Module {
+ let mut math = Scope::deduplicating();
+ math.define("equation", EquationElem::func());
+ math.define("text", TextElem::func());
+
+ // Grouping.
+ math.define("lr", LrElem::func());
+ math.define("abs", abs_func());
+ math.define("norm", norm_func());
+ math.define("floor", floor_func());
+ math.define("ceil", ceil_func());
+ math.define("round", round_func());
+
+ // Attachments and accents.
+ math.define("attach", AttachElem::func());
+ math.define("scripts", ScriptsElem::func());
+ math.define("limits", LimitsElem::func());
+ math.define("accent", AccentElem::func());
+ math.define("underline", UnderlineElem::func());
+ math.define("overline", OverlineElem::func());
+ math.define("underbrace", UnderbraceElem::func());
+ math.define("overbrace", OverbraceElem::func());
+ math.define("underbracket", UnderbracketElem::func());
+ math.define("overbracket", OverbracketElem::func());
+ math.define("cancel", CancelElem::func());
+
+ // Fractions and matrix-likes.
+ math.define("frac", FracElem::func());
+ math.define("binom", BinomElem::func());
+ math.define("vec", VecElem::func());
+ math.define("mat", MatElem::func());
+ math.define("cases", CasesElem::func());
+
+ // Roots.
+ math.define("sqrt", sqrt_func());
+ math.define("root", RootElem::func());
+
+ // Styles.
+ math.define("upright", upright_func());
+ math.define("bold", bold_func());
+ math.define("italic", italic_func());
+ math.define("serif", serif_func());
+ math.define("sans", sans_func());
+ math.define("cal", cal_func());
+ math.define("frak", frak_func());
+ math.define("mono", mono_func());
+ math.define("bb", bb_func());
+
+ math.define("display", display_func());
+ math.define("inline", inline_func());
+ math.define("script", script_func());
+ math.define("sscript", sscript_func());
+
+ // Text operators.
+ math.define("op", OpElem::func());
+ op::define(&mut math);
+
+ // Spacings.
+ spacing::define(&mut math);
+
+ // Symbols.
+ for (name, symbol) in crate::symbols::SYM {
+ math.define(*name, symbol.clone());
+ }
+
+ Module::new("math").with_scope(math)
+}
+
+/// A mathematical equation.
+///
+/// Can be displayed inline with text or as a separate block.
+///
+/// ## Example { #example }
+/// ```example
+/// #set text(font: "New Computer Modern")
+///
+/// Let $a$, $b$, and $c$ be the side
+/// lengths of right-angled triangle.
+/// Then, we know that:
+/// $ a^2 + b^2 = c^2 $
+///
+/// Prove by induction:
+/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax: Write mathematical markup within
+/// dollar signs to create an equation. Starting and ending the equation with at
+/// least one space lifts it into a separate block that is centered
+/// horizontally. For more details about math syntax, see the
+/// [main math page]($category/math).
+///
+/// Display: Equation
+/// Category: math
+#[element(
+ Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable,
+ Outlinable
+)]
+pub struct EquationElem {
+ /// Whether the equation is displayed as a separate block.
+ #[default(false)]
+ pub block: bool,
+
+ /// How to [number]($func/numbering) block-level equations.
+ ///
+ /// ```example
+ /// #set math.equation(numbering: "(1)")
+ ///
+ /// We define:
+ /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
+ ///
+ /// With @ratio, we get:
+ /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
+ /// ```
+ pub numbering: Option<Numbering>,
+
+ /// A supplement for the equation.
+ ///
+ /// For references to equations, this is added before the referenced number.
+ ///
+ /// If a function is specified, it is passed the referenced equation and
+ /// should return content.
+ ///
+ /// ```example
+ /// #set math.equation(numbering: "(1)", supplement: [Eq.])
+ ///
+ /// We define:
+ /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
+ ///
+ /// With @ratio, we get:
+ /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
+ /// ```
+ pub supplement: Smart<Option<Supplement>>,
+
+ /// The contents of the equation.
+ #[required]
+ pub body: Content,
+}
+
+impl Synthesize for EquationElem {
+ fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ // Resolve the supplement.
+ let supplement = match self.supplement(styles) {
+ Smart::Auto => TextElem::packed(self.local_name_in(styles)),
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?,
+ };
+
+ self.push_block(self.block(styles));
+ self.push_numbering(self.numbering(styles));
+ self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
+
+ Ok(())
+ }
+}
+
+impl Show for EquationElem {
+ #[tracing::instrument(name = "EquationElem::show", skip_all)]
+ fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ let mut realized = self.clone().pack().guarded(Guard::Base(Self::func()));
+ if self.block(styles) {
+ realized = realized.aligned(Axes::with_x(Some(Align::Center.into())))
+ }
+ Ok(realized)
+ }
+}
+
+impl Finalize for EquationElem {
+ fn finalize(&self, realized: Content, _: StyleChain) -> Content {
+ realized
+ .styled(TextElem::set_weight(FontWeight::from_number(450)))
+ .styled(TextElem::set_font(FontList(vec![FontFamily::new(
+ "New Computer Modern Math",
+ )])))
+ }
+}
+
+impl Layout for EquationElem {
+ #[tracing::instrument(name = "EquationElem::layout", skip_all)]
+ fn layout(
+ &self,
+ vt: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ const NUMBER_GUTTER: Em = Em::new(0.5);
+
+ let block = self.block(styles);
+
+ // Find a math font.
+ let variant = variant(styles);
+ let world = vt.world;
+ let Some(font) = families(styles)
+ .find_map(|family| {
+ let id = world.book().select(family.as_str(), variant)?;
+ let font = world.font(id)?;
+ let _ = font.ttf().tables().math?.constants?;
+ Some(font)
+ })
+ else {
+ bail!(self.span(), "current font does not support math");
+ };
+
+ let mut ctx = MathContext::new(vt, styles, regions, &font, block);
+ let mut frame = ctx.layout_frame(self)?;
+
+ if block {
+ if let Some(numbering) = self.numbering(styles) {
+ let pod = Regions::one(regions.base(), Axes::splat(false));
+ let counter = Counter::of(Self::func())
+ .display(Some(numbering), false)
+ .layout(vt, styles, pod)?
+ .into_frame();
+
+ let width = if regions.size.x.is_finite() {
+ regions.size.x
+ } else {
+ frame.width()
+ + 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles))
+ };
+
+ let height = frame.height().max(counter.height());
+ frame.resize(Size::new(width, height), Align::CENTER_HORIZON);
+
+ let x = if TextElem::dir_in(styles).is_positive() {
+ frame.width() - counter.width()
+ } else {
+ Abs::zero()
+ };
+ let y = (frame.height() - counter.height()) / 2.0;
+
+ frame.push_frame(Point::new(x, y), counter)
+ }
+ } else {
+ let slack = ParElem::leading_in(styles) * 0.7;
+ let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics());
+ let bottom_edge =
+ -TextElem::bottom_edge_in(styles).resolve(styles, font.metrics());
+
+ let ascent = top_edge.max(frame.ascent() - slack);
+ let descent = bottom_edge.max(frame.descent() - slack);
+ frame.translate(Point::with_y(ascent - frame.baseline()));
+ frame.size_mut().y = ascent + descent;
+ }
+
+ // Apply metadata.
+ frame.meta(styles, false);
+
+ Ok(Fragment::frame(frame))
+ }
+}
+
+impl Count for EquationElem {
+ fn update(&self) -> Option<CounterUpdate> {
+ (self.block(StyleChain::default())
+ && self.numbering(StyleChain::default()).is_some())
+ .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
+ }
+}
+
+impl LocalName for EquationElem {
+ fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str {
+ match lang {
+ Lang::ALBANIAN => "Ekuacion",
+ Lang::ARABIC => "معادلة",
+ Lang::BOKMÅL => "Ligning",
+ Lang::CHINESE if option_eq(region, "TW") => "方程式",
+ Lang::CHINESE => "等式",
+ Lang::CZECH => "Rovnice",
+ Lang::DANISH => "Ligning",
+ Lang::DUTCH => "Vergelijking",
+ Lang::FILIPINO => "Ekwasyon",
+ Lang::FRENCH => "Équation",
+ Lang::GERMAN => "Gleichung",
+ Lang::ITALIAN => "Equazione",
+ Lang::NYNORSK => "Likning",
+ Lang::POLISH => "Równanie",
+ Lang::PORTUGUESE => "Equação",
+ Lang::RUSSIAN => "Уравнение",
+ Lang::SLOVENIAN => "Enačba",
+ Lang::SPANISH => "Ecuación",
+ Lang::SWEDISH => "Ekvation",
+ Lang::TURKISH => "Denklem",
+ Lang::UKRAINIAN => "Рівняння",
+ Lang::VIETNAMESE => "Phương trình",
+ Lang::ENGLISH | _ => "Equation",
+ }
+ }
+}
+
+impl Refable for EquationElem {
+ fn supplement(&self) -> Content {
+ // After synthesis, this should always be custom content.
+ match self.supplement(StyleChain::default()) {
+ Smart::Custom(Some(Supplement::Content(content))) => content,
+ _ => Content::empty(),
+ }
+ }
+
+ fn counter(&self) -> Counter {
+ Counter::of(Self::func())
+ }
+
+ fn numbering(&self) -> Option<Numbering> {
+ self.numbering(StyleChain::default())
+ }
+}
+
+impl Outlinable for EquationElem {
+ fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
+ let Some(numbering) = self.numbering(StyleChain::default()) else {
+ return Ok(None);
+ };
+
+ // After synthesis, this should always be custom content.
+ let mut supplement = match self.supplement(StyleChain::default()) {
+ Smart::Custom(Some(Supplement::Content(content))) => content,
+ _ => Content::empty(),
+ };
+
+ if !supplement.is_empty() {
+ supplement += TextElem::packed("\u{a0}");
+ }
+
+ let numbers = self
+ .counter()
+ .at(vt, self.0.location().unwrap())?
+ .display(vt, &numbering)?;
+
+ Ok(Some(supplement + numbers))
+ }
+}
+
+pub trait LayoutMath {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
+}
+
+impl LayoutMath for EquationElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ self.body().layout_math(ctx)
+ }
+}
+
+impl LayoutMath for Content {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ // Directly layout the body of nested equations instead of handling it
+ // like a normal equation so that things like this work:
+ // ```
+ // #let my = $pi$
+ // $ my r^2 $
+ // ```
+ if let Some(elem) = self.to::<EquationElem>() {
+ return elem.layout_math(ctx);
+ }
+
+ if let Some(realized) = ctx.realize(self)? {
+ return realized.layout_math(ctx);
+ }
+
+ if let Some(children) = self.to_sequence() {
+ for child in children {
+ child.layout_math(ctx)?;
+ }
+ return Ok(());
+ }
+
+ if let Some((elem, styles)) = self.to_styled() {
+ if TextElem::font_in(ctx.styles().chain(styles))
+ != TextElem::font_in(ctx.styles())
+ {
+ let frame = ctx.layout_content(self)?;
+ ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
+ return Ok(());
+ }
+
+ let prev_map = std::mem::replace(&mut ctx.local, styles.clone());
+ let prev_size = ctx.size;
+ ctx.local.apply(prev_map.clone());
+ ctx.size = TextElem::size_in(ctx.styles());
+ elem.layout_math(ctx)?;
+ ctx.size = prev_size;
+ ctx.local = prev_map;
+ return Ok(());
+ }
+
+ if self.is::<SpaceElem>() {
+ ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx)));
+ return Ok(());
+ }
+
+ if self.is::<LinebreakElem>() {
+ ctx.push(MathFragment::Linebreak);
+ return Ok(());
+ }
+
+ if let Some(elem) = self.to::<HElem>() {
+ if let Spacing::Rel(rel) = elem.amount() {
+ if rel.rel.is_zero() {
+ ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles())));
+ }
+ }
+ return Ok(());
+ }
+
+ if let Some(elem) = self.to::<TextElem>() {
+ let fragment = ctx.layout_text(elem)?;
+ ctx.push(fragment);
+ return Ok(());
+ }
+
+ if let Some(elem) = self.with::<dyn LayoutMath>() {
+ return elem.layout_math(ctx);
+ }
+
+ let mut frame = ctx.layout_content(self)?;
+ if !frame.has_baseline() {
+ let axis = scaled!(ctx, axis_height);
+ frame.set_baseline(frame.height() / 2.0 + axis);
+ }
+ ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
+
+ Ok(())
+ }
+}