summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/math')
-rw-r--r--crates/typst-library/src/math/accent.rs147
-rw-r--r--crates/typst-library/src/math/attach.rs156
-rw-r--r--crates/typst-library/src/math/cancel.rs116
-rw-r--r--crates/typst-library/src/math/equation.rs256
-rw-r--r--crates/typst-library/src/math/frac.rs56
-rw-r--r--crates/typst-library/src/math/lr.rs135
-rw-r--r--crates/typst-library/src/math/matrix.rs448
-rw-r--r--crates/typst-library/src/math/mod.rs257
-rw-r--r--crates/typst-library/src/math/op.rs103
-rw-r--r--crates/typst-library/src/math/root.rs35
-rw-r--r--crates/typst-library/src/math/style.rs254
-rw-r--r--crates/typst-library/src/math/underover.rs156
12 files changed, 2119 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs
new file mode 100644
index 00000000..a1c70b4a
--- /dev/null
+++ b/crates/typst-library/src/math/accent.rs
@@ -0,0 +1,147 @@
+use crate::diag::bail;
+use crate::foundations::{cast, elem, func, Content, NativeElement, Smart, Value};
+use crate::layout::{Length, Rel};
+use crate::math::Mathy;
+use crate::text::TextElem;
+
+/// Attaches an accent to a base.
+///
+/// # Example
+/// ```example
+/// $grave(a) = accent(a, `)$ \
+/// $arrow(a) = accent(a, arrow)$ \
+/// $tilde(a) = accent(a, \u{0303})$
+/// ```
+#[elem(Mathy)]
+pub struct AccentElem {
+ /// The base to which the accent is applied.
+ /// May consist of multiple letters.
+ ///
+ /// ```example
+ /// $arrow(A B C)$
+ /// ```
+ #[required]
+ pub base: Content,
+
+ /// The accent to apply to the base.
+ ///
+ /// Supported accents include:
+ ///
+ /// | Accent | Name | Codepoint |
+ /// | ------------- | --------------- | --------- |
+ /// | Grave | `grave` | <code>&DiacriticalGrave;</code> |
+ /// | Acute | `acute` | `´` |
+ /// | Circumflex | `hat` | `^` |
+ /// | Tilde | `tilde` | `~` |
+ /// | Macron | `macron` | `¯` |
+ /// | Dash | `dash` | `‾` |
+ /// | Breve | `breve` | `˘` |
+ /// | Dot | `dot` | `.` |
+ /// | Double dot, Diaeresis | `dot.double`, `diaer` | `¨` |
+ /// | Triple dot | `dot.triple` | <code>&tdot;</code> |
+ /// | Quadruple dot | `dot.quad` | <code>&DotDot;</code> |
+ /// | Circle | `circle` | `∘` |
+ /// | Double acute | `acute.double` | `˝` |
+ /// | Caron | `caron` | `ˇ` |
+ /// | Right arrow | `arrow`, `->` | `→` |
+ /// | Left arrow | `arrow.l`, `<-` | `←` |
+ /// | Left/Right arrow | `arrow.l.r` | `↔` |
+ /// | Right harpoon | `harpoon` | `⇀` |
+ /// | Left harpoon | `harpoon.lt` | `↼` |
+ #[required]
+ pub accent: Accent,
+
+ /// The size of the accent, relative to the width of the base.
+ pub size: Smart<Rel<Length>>,
+}
+
+/// An accent character.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Accent(pub char);
+
+impl Accent {
+ /// Normalize a character into an accent.
+ pub fn new(c: char) -> Self {
+ Self(Self::combine(c).unwrap_or(c))
+ }
+}
+
+/// This macro generates accent-related functions.
+///
+/// ```ignore
+/// accents! {
+/// '\u{0300}' | '`' => grave,
+/// // ^^^^^^^^^ ^^^ ^^^^^
+/// // | | |
+/// // | | +-- The name of the function.
+/// // | +--------- The alternative characters that represent the accent.
+/// // +---------------------- The primary character that represents the accent.
+/// }
+/// ```
+///
+/// When combined with the `Accent::combine` function, accent characters can be normalized
+/// to the primary character.
+macro_rules! accents {
+ ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => {
+ impl Accent {
+ /// Normalize an accent to a combining one.
+ pub fn combine(c: char) -> Option<char> {
+ Some(match c {
+ $($primary $(| $alt)* => $primary,)*
+ _ => return None,
+ })
+ }
+ }
+
+ $(
+ /// The accent function for callable symbol definitions.
+ #[func]
+ pub fn $name(
+ /// The base to which the accent is applied.
+ base: Content,
+ /// The size of the accent, relative to the width of the base.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ ) -> Content {
+ let mut accent = AccentElem::new(base, Accent::new($primary));
+ if let Some(size) = size {
+ accent = accent.with_size(size);
+ }
+ accent.pack()
+ }
+ )+
+ };
+}
+
+// Keep it synced with the documenting table above.
+accents! {
+ '\u{0300}' | '`' => grave,
+ '\u{0301}' | '´' => acute,
+ '\u{0302}' | '^' | 'ˆ' => hat,
+ '\u{0303}' | '~' | '∼' | '˜' => tilde,
+ '\u{0304}' | '¯' => macron,
+ '\u{0305}' | '-' | '‾' | '−' => dash,
+ '\u{0306}' | '˘' => breve,
+ '\u{0307}' | '.' | '˙' | '⋅' => dot,
+ '\u{0308}' | '¨' => dot_double,
+ '\u{20db}' => dot_triple,
+ '\u{20dc}' => dot_quad,
+ '\u{030a}' | '∘' | '○' => circle,
+ '\u{030b}' | '˝' => acute_double,
+ '\u{030c}' | 'ˇ' => caron,
+ '\u{20d6}' | '←' => arrow_l,
+ '\u{20d7}' | '→' | '⟶' => arrow,
+ '\u{20e1}' | '↔' | '⟷' => arrow_l_r,
+ '\u{20d0}' | '↼' => harpoon_lt,
+ '\u{20d1}' | '⇀' => harpoon,
+}
+
+cast! {
+ Accent,
+ self => self.0.into_value(),
+ v: char => Self::new(v),
+ v: Content => match v.to_packed::<TextElem>() {
+ Some(elem) => Value::Str(elem.text().clone().into()).cast()?,
+ None => bail!("expected text"),
+ },
+}
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
new file mode 100644
index 00000000..74eabd10
--- /dev/null
+++ b/crates/typst-library/src/math/attach.rs
@@ -0,0 +1,156 @@
+use crate::foundations::{elem, Content, Packed, Smart};
+use crate::layout::{Length, Rel};
+use crate::math::{EquationElem, Mathy};
+
+/// A base with optional attachments.
+///
+/// ```example
+/// $ attach(
+/// Pi, t: alpha, b: beta,
+/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
+/// ) $
+/// ```
+#[elem(Mathy)]
+pub struct AttachElem {
+ /// The base to which things are attached.
+ #[required]
+ pub base: Content,
+
+ /// The top attachment, smartly positioned at top-right or above the base.
+ ///
+ /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
+ /// smart positioning.
+ pub t: Option<Content>,
+
+ /// The bottom attachment, smartly positioned at the bottom-right or below
+ /// the base.
+ ///
+ /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
+ /// smart positioning.
+ pub b: Option<Content>,
+
+ /// The top-left attachment (before the base).
+ pub tl: Option<Content>,
+
+ /// The bottom-left attachment (before base).
+ pub bl: Option<Content>,
+
+ /// The top-right attachment (after the base).
+ pub tr: Option<Content>,
+
+ /// The bottom-right attachment (after the base).
+ pub br: Option<Content>,
+}
+
+impl Packed<AttachElem> {
+ /// If an AttachElem's base is also an AttachElem, merge attachments into the
+ /// base AttachElem where possible.
+ pub fn merge_base(&self) -> Option<Self> {
+ // Extract from an EquationElem.
+ let mut base = self.base();
+ if let Some(equation) = base.to_packed::<EquationElem>() {
+ base = equation.body();
+ }
+
+ // Move attachments from elem into base where possible.
+ if let Some(base) = base.to_packed::<AttachElem>() {
+ let mut elem = self.clone();
+ let mut base = base.clone();
+
+ macro_rules! merge {
+ ($content:ident) => {
+ if base.$content.is_none() && elem.$content.is_some() {
+ base.$content = elem.$content.clone();
+ elem.$content = None;
+ }
+ };
+ }
+
+ merge!(t);
+ merge!(b);
+ merge!(tl);
+ merge!(tr);
+ merge!(bl);
+ merge!(br);
+
+ elem.base = base.pack();
+ return Some(elem);
+ }
+
+ None
+ }
+}
+
+/// Grouped primes.
+///
+/// ```example
+/// $ a'''_b = a^'''_b $
+/// ```
+///
+/// # Syntax
+/// This function has dedicated syntax: use apostrophes instead of primes. They
+/// will automatically attach to the previous element, moving superscripts to
+/// the next level.
+#[elem(Mathy)]
+pub struct PrimesElem {
+ /// The number of grouped primes.
+ #[required]
+ pub count: usize,
+}
+
+/// Forces a base to display attachments as scripts.
+///
+/// ```example
+/// $ scripts(sum)_1^2 != sum_1^2 $
+/// ```
+#[elem(Mathy)]
+pub struct ScriptsElem {
+ /// The base to attach the scripts to.
+ #[required]
+ pub body: Content,
+}
+
+/// Forces a base to display attachments as limits.
+///
+/// ```example
+/// $ limits(A)_1^2 != A_1^2 $
+/// ```
+#[elem(Mathy)]
+pub struct LimitsElem {
+ /// The base to attach the limits to.
+ #[required]
+ pub body: Content,
+
+ /// Whether to also force limits in inline equations.
+ ///
+ /// When applying limits globally (e.g., through a show rule), it is
+ /// typically a good idea to disable this.
+ #[default(true)]
+ pub inline: bool,
+}
+
+/// Stretches a glyph.
+///
+/// This function can also be used to automatically stretch the base of an
+/// attachment, so that it fits the top and bottom attachments.
+///
+/// Note that only some glyphs can be stretched, and which ones can depend on
+/// the math font being used. However, most math fonts are the same in this
+/// regard.
+///
+/// ```example
+/// $ H stretch(=)^"define" U + p V $
+/// $ f : X stretch(->>, size: #150%)_"surjective" Y $
+/// $ x stretch(harpoons.ltrb, size: #3em) y
+/// stretch(\[, size: #150%) z $
+/// ```
+#[elem(Mathy)]
+pub struct StretchElem {
+ /// The glyph to stretch.
+ #[required]
+ pub body: Content,
+
+ /// The size to stretch to, relative to the maximum size of the glyph and
+ /// its attachments.
+ pub size: Smart<Rel<Length>>,
+}
diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs
new file mode 100644
index 00000000..a72505c0
--- /dev/null
+++ b/crates/typst-library/src/math/cancel.rs
@@ -0,0 +1,116 @@
+use crate::foundations::{cast, elem, Content, Func, Smart};
+use crate::layout::{Abs, Angle, Length, Ratio, Rel};
+use crate::math::Mathy;
+use crate::visualize::Stroke;
+
+/// Displays a diagonal line over a part of an equation.
+///
+/// This is commonly used to show the elimination of a term.
+///
+/// # Example
+/// ```example
+/// >>> #set page(width: 140pt)
+/// Here, we can simplify:
+/// $ (a dot b dot cancel(x)) /
+/// cancel(x) $
+/// ```
+#[elem(Mathy)]
+pub struct CancelElem {
+ /// The content over which the line should be placed.
+ #[required]
+ pub body: Content,
+
+ /// The length of the line, relative to the length of the diagonal spanning
+ /// the whole element being "cancelled". A value of `{100%}` would then have
+ /// the line span precisely the element's diagonal.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ a + cancel(x, length: #200%)
+ /// - cancel(x, length: #200%) $
+ /// ```
+ #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))]
+ pub length: Rel<Length>,
+
+ /// Whether the cancel line should be inverted (flipped along the y-axis).
+ /// For the default angle setting, inverted means the cancel line
+ /// points to the top left instead of top right.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ (a cancel((b + c), inverted: #true)) /
+ /// cancel(b + c, inverted: #true) $
+ /// ```
+ #[default(false)]
+ pub inverted: bool,
+
+ /// Whether two opposing cancel lines should be drawn, forming a cross over
+ /// the element. Overrides `inverted`.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ cancel(Pi, cross: #true) $
+ /// ```
+ #[default(false)]
+ pub cross: bool,
+
+ /// How much to rotate the cancel line.
+ ///
+ /// - If given an angle, the line is rotated by that angle clockwise with
+ /// respect to the y-axis.
+ /// - If `{auto}`, the line assumes the default angle; that is, along the
+ /// rising diagonal of the content box.
+ /// - If given a function `angle => angle`, the line is rotated, with
+ /// respect to the y-axis, by the angle returned by that function. The
+ /// function receives the default angle as its input.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ cancel(Pi)
+ /// cancel(Pi, angle: #0deg)
+ /// cancel(Pi, angle: #45deg)
+ /// cancel(Pi, angle: #90deg)
+ /// cancel(1/(1+x), angle: #(a => a + 45deg))
+ /// cancel(1/(1+x), angle: #(a => a + 90deg)) $
+ /// ```
+ pub angle: Smart<CancelAngle>,
+
+ /// How to [stroke]($stroke) the cancel line.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ cancel(
+ /// sum x,
+ /// stroke: #(
+ /// paint: red,
+ /// thickness: 1.5pt,
+ /// dash: "dashed",
+ /// ),
+ /// ) $
+ /// ```
+ #[resolve]
+ #[fold]
+ #[default(Stroke {
+ // Default stroke has 0.5pt for better visuals.
+ thickness: Smart::Custom(Abs::pt(0.5).into()),
+ ..Default::default()
+ })]
+ pub stroke: Stroke,
+}
+
+/// Defines the cancel line.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum CancelAngle {
+ Angle(Angle),
+ Func(Func),
+}
+
+cast! {
+ CancelAngle,
+ self => match self {
+ Self::Angle(v) => v.into_value(),
+ Self::Func(v) => v.into_value()
+ },
+ v: Angle => CancelAngle::Angle(v),
+ v: Func => CancelAngle::Func(v),
+}
diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs
new file mode 100644
index 00000000..4edafe5e
--- /dev/null
+++ b/crates/typst-library/src/math/equation.rs
@@ -0,0 +1,256 @@
+use std::num::NonZeroUsize;
+
+use typst_utils::NonZeroExt;
+use unicode_math_class::MathClass;
+
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{
+ elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
+ Synthesize,
+};
+use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
+use crate::layout::{
+ AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment,
+ VAlignment,
+};
+use crate::math::{MathSize, MathVariant};
+use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
+use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
+
+/// A mathematical equation.
+///
+/// Can be displayed inline with text or as a separate block.
+///
+/// # 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 $
+/// ```
+///
+/// By default, block-level equations will not break across pages. This can be
+/// changed through `{show math.equation: set block(breakable: true)}`.
+///
+/// # 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).
+#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)]
+pub struct EquationElem {
+ /// Whether the equation is displayed as a separate block.
+ #[default(false)]
+ pub block: bool,
+
+ /// How to [number]($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) $
+ /// ```
+ #[borrowed]
+ pub numbering: Option<Numbering>,
+
+ /// The alignment of the equation numbering.
+ ///
+ /// By default, the alignment is `{end + horizon}`. For the horizontal
+ /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}`
+ /// of the text direction; for the vertical component, you can use
+ /// `{top}`, `{horizon}`, or `{bottom}`.
+ ///
+ /// ```example
+ /// #set math.equation(numbering: "(1)", number-align: bottom)
+ ///
+ /// We can calculate:
+ /// $ E &= sqrt(m_0^2 + p^2) \
+ /// &approx 125 "GeV" $
+ /// ```
+ #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))]
+ pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>,
+
+ /// 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,
+
+ /// The size of the glyphs.
+ #[internal]
+ #[default(MathSize::Text)]
+ #[ghost]
+ pub size: MathSize,
+
+ /// The style variant to select.
+ #[internal]
+ #[ghost]
+ pub variant: MathVariant,
+
+ /// Affects the height of exponents.
+ #[internal]
+ #[default(false)]
+ #[ghost]
+ pub cramped: bool,
+
+ /// Whether to use bold glyphs.
+ #[internal]
+ #[default(false)]
+ #[ghost]
+ pub bold: bool,
+
+ /// Whether to use italic glyphs.
+ #[internal]
+ #[ghost]
+ pub italic: Smart<bool>,
+
+ /// A forced class to use for all fragment.
+ #[internal]
+ #[ghost]
+ pub class: Option<MathClass>,
+}
+
+impl Synthesize for Packed<EquationElem> {
+ fn synthesize(
+ &mut self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ ) -> SourceResult<()> {
+ let supplement = match self.as_ref().supplement(styles) {
+ Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
+ Smart::Custom(None) => Content::empty(),
+ Smart::Custom(Some(supplement)) => {
+ supplement.resolve(engine, styles, [self.clone().pack()])?
+ }
+ };
+
+ self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
+ Ok(())
+ }
+}
+
+impl Show for Packed<EquationElem> {
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ if self.block(styles) {
+ Ok(BlockElem::multi_layouter(
+ self.clone(),
+ engine.routines.layout_equation_block,
+ )
+ .pack()
+ .spanned(self.span()))
+ } else {
+ Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline)
+ .pack()
+ .spanned(self.span()))
+ }
+ }
+}
+
+impl ShowSet for Packed<EquationElem> {
+ fn show_set(&self, styles: StyleChain) -> Styles {
+ let mut out = Styles::new();
+ if self.block(styles) {
+ out.set(AlignElem::set_alignment(Alignment::CENTER));
+ out.set(BlockElem::set_breakable(false));
+ out.set(ParLine::set_numbering(None));
+ out.set(EquationElem::set_size(MathSize::Display));
+ } else {
+ out.set(EquationElem::set_size(MathSize::Text));
+ }
+ out.set(TextElem::set_weight(FontWeight::from_number(450)));
+ out.set(TextElem::set_font(FontList(vec![FontFamily::new(
+ "New Computer Modern Math",
+ )])));
+ out
+ }
+}
+
+impl Count for Packed<EquationElem> {
+ fn update(&self) -> Option<CounterUpdate> {
+ (self.block(StyleChain::default()) && self.numbering().is_some())
+ .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
+ }
+}
+
+impl LocalName for Packed<EquationElem> {
+ const KEY: &'static str = "equation";
+}
+
+impl Refable for Packed<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(EquationElem::elem())
+ }
+
+ fn numbering(&self) -> Option<&Numbering> {
+ (**self).numbering(StyleChain::default()).as_ref()
+ }
+}
+
+impl Outlinable for Packed<EquationElem> {
+ fn outline(
+ &self,
+ engine: &mut Engine,
+ styles: StyleChain,
+ ) -> SourceResult<Option<Content>> {
+ if !self.block(StyleChain::default()) {
+ return Ok(None);
+ }
+ let Some(numbering) = self.numbering() 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().display_at_loc(
+ engine,
+ self.location().unwrap(),
+ styles,
+ numbering,
+ )?;
+
+ Ok(Some(supplement + numbers))
+ }
+}
diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs
new file mode 100644
index 00000000..f5c4514d
--- /dev/null
+++ b/crates/typst-library/src/math/frac.rs
@@ -0,0 +1,56 @@
+use typst_syntax::Spanned;
+
+use crate::diag::bail;
+use crate::foundations::{elem, Content, Value};
+use crate::math::Mathy;
+
+/// A mathematical fraction.
+///
+/// # Example
+/// ```example
+/// $ 1/2 < (x+1)/2 $
+/// $ ((x+1)) / 2 = frac(a, b) $
+/// ```
+///
+/// # Syntax
+/// This function also has dedicated syntax: Use a slash to turn neighbouring
+/// expressions into a fraction. Multiple atoms can be grouped into a single
+/// expression using round grouping parenthesis. Such parentheses are removed
+/// from the output, but you can nest multiple to force them.
+#[elem(title = "Fraction", Mathy)]
+pub struct FracElem {
+ /// The fraction's numerator.
+ #[required]
+ pub num: Content,
+
+ /// The fraction's denominator.
+ #[required]
+ pub denom: Content,
+}
+
+/// A binomial expression.
+///
+/// # Example
+/// ```example
+/// $ binom(n, k) $
+/// $ binom(n, k_1, k_2, k_3, ..., k_m) $
+/// ```
+#[elem(title = "Binomial", Mathy)]
+pub struct BinomElem {
+ /// The binomial's upper index.
+ #[required]
+ pub upper: Content,
+
+ /// The binomial's lower index.
+ #[required]
+ #[variadic]
+ #[parse(
+ let values = args.all::<Spanned<Value>>()?;
+ if values.is_empty() {
+ // Prevents one element binomials
+ bail!(args.span, "missing argument: lower");
+ }
+ values.into_iter().map(|spanned| spanned.v.display()).collect()
+ )]
+ pub lower: Vec<Content>,
+}
diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs
new file mode 100644
index 00000000..07ab0dd4
--- /dev/null
+++ b/crates/typst-library/src/math/lr.rs
@@ -0,0 +1,135 @@
+use crate::foundations::{elem, func, Content, NativeElement, Smart};
+use crate::layout::{Length, Rel};
+use crate::math::Mathy;
+use crate::text::TextElem;
+
+/// Scales delimiters.
+///
+/// While matched delimiters scale by default, this can be used to scale
+/// unmatched delimiters and to control the delimiter scaling more precisely.
+#[elem(title = "Left/Right", Mathy)]
+pub struct LrElem {
+ /// The size of the brackets, relative to the height of the wrapped content.
+ pub size: Smart<Rel<Length>>,
+
+ /// The delimited content, including the delimiters.
+ #[required]
+ #[parse(
+ let mut arguments = args.all::<Content>()?.into_iter();
+ let mut body = arguments.next().unwrap_or_default();
+ arguments.for_each(|arg| body += TextElem::packed(',') + arg);
+ body
+ )]
+ pub body: Content,
+}
+
+/// Scales delimiters vertically to the nearest surrounding `{lr()}` group.
+///
+/// ```example
+/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $
+/// ```
+#[elem(Mathy)]
+pub struct MidElem {
+ /// The content to be scaled.
+ #[required]
+ pub body: Content,
+}
+
+/// Floors an expression.
+///
+/// ```example
+/// $ floor(x/2) $
+/// ```
+#[func]
+pub fn floor(
+ /// The size of the brackets, relative to the height of the wrapped content.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ /// The expression to floor.
+ body: Content,
+) -> Content {
+ delimited(body, '⌊', '⌋', size)
+}
+
+/// Ceils an expression.
+///
+/// ```example
+/// $ ceil(x/2) $
+/// ```
+#[func]
+pub fn ceil(
+ /// The size of the brackets, relative to the height of the wrapped content.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ /// The expression to ceil.
+ body: Content,
+) -> Content {
+ delimited(body, '⌈', '⌉', size)
+}
+
+/// Rounds an expression.
+///
+/// ```example
+/// $ round(x/2) $
+/// ```
+#[func]
+pub fn round(
+ /// The size of the brackets, relative to the height of the wrapped content.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ /// The expression to round.
+ body: Content,
+) -> Content {
+ delimited(body, '⌊', '⌉', size)
+}
+
+/// Takes the absolute value of an expression.
+///
+/// ```example
+/// $ abs(x/2) $
+/// ```
+#[func]
+pub fn abs(
+ /// The size of the brackets, relative to the height of the wrapped content.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ /// The expression to take the absolute value of.
+ body: Content,
+) -> Content {
+ delimited(body, '|', '|', size)
+}
+
+/// Takes the norm of an expression.
+///
+/// ```example
+/// $ norm(x/2) $
+/// ```
+#[func]
+pub fn norm(
+ /// The size of the brackets, relative to the height of the wrapped content.
+ #[named]
+ size: Option<Smart<Rel<Length>>>,
+ /// The expression to take the norm of.
+ body: Content,
+) -> Content {
+ delimited(body, '‖', '‖', size)
+}
+
+fn delimited(
+ body: Content,
+ left: char,
+ right: char,
+ size: Option<Smart<Rel<Length>>>,
+) -> Content {
+ let span = body.span();
+ let mut elem = LrElem::new(Content::sequence([
+ TextElem::packed(left),
+ body,
+ TextElem::packed(right),
+ ]));
+ // Push size only if size is provided
+ if let Some(size) = size {
+ elem.push_size(size);
+ }
+ elem.pack().spanned(span)
+}
diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs
new file mode 100644
index 00000000..1c788995
--- /dev/null
+++ b/crates/typst-library/src/math/matrix.rs
@@ -0,0 +1,448 @@
+use smallvec::{smallvec, SmallVec};
+use typst_syntax::Spanned;
+use typst_utils::Numeric;
+use unicode_math_class::MathClass;
+
+use crate::diag::{bail, At, HintedStrResult, StrResult};
+use crate::foundations::{
+ array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Resolve, Smart,
+ StyleChain, Symbol, Value,
+};
+use crate::layout::{Abs, Em, HAlignment, Length, Rel};
+use crate::math::Mathy;
+use crate::visualize::Stroke;
+
+const DEFAULT_ROW_GAP: Em = Em::new(0.2);
+const DEFAULT_COL_GAP: Em = Em::new(0.5);
+
+/// A column vector.
+///
+/// Content in the vector's elements can be aligned with the
+/// [`align`]($math.vec.align) parameter, or the `&` symbol.
+///
+/// # Example
+/// ```example
+/// $ vec(a, b, c) dot vec(1, 2, 3)
+/// = a + 2b + 3c $
+/// ```
+#[elem(title = "Vector", Mathy)]
+pub struct VecElem {
+ /// The delimiter to use.
+ ///
+ /// Can be a single character specifying the left delimiter, in which case
+ /// the right delimiter is inferred. Otherwise, can be an array containing a
+ /// left and a right delimiter.
+ ///
+ /// ```example
+ /// #set math.vec(delim: "[")
+ /// $ vec(1, 2) $
+ /// ```
+ #[default(DelimiterPair::PAREN)]
+ pub delim: DelimiterPair,
+
+ /// The horizontal alignment that each element should have.
+ ///
+ /// ```example
+ /// #set math.vec(align: right)
+ /// $ vec(-1, 1, -1) $
+ /// ```
+ #[resolve]
+ #[default(HAlignment::Center)]
+ pub align: HAlignment,
+
+ /// The gap between elements.
+ ///
+ /// ```example
+ /// #set math.vec(gap: 1em)
+ /// $ vec(1, 2) $
+ /// ```
+ #[default(DEFAULT_ROW_GAP.into())]
+ pub gap: Rel<Length>,
+
+ /// The elements of the vector.
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+/// A matrix.
+///
+/// The elements of a row should be separated by commas, while the rows
+/// themselves should be separated by semicolons. The semicolon syntax merges
+/// preceding arguments separated by commas into an array. You can also use this
+/// special syntax of math function calls to define custom functions that take
+/// 2D data.
+///
+/// Content in cells can be aligned with the [`align`]($math.mat.align)
+/// parameter, or content in cells that are in the same row can be aligned with
+/// the `&` symbol.
+///
+/// # Example
+/// ```example
+/// $ mat(
+/// 1, 2, ..., 10;
+/// 2, 2, ..., 10;
+/// dots.v, dots.v, dots.down, dots.v;
+/// 10, 10, ..., 10;
+/// ) $
+/// ```
+#[elem(title = "Matrix", Mathy)]
+pub struct MatElem {
+ /// The delimiter to use.
+ ///
+ /// Can be a single character specifying the left delimiter, in which case
+ /// the right delimiter is inferred. Otherwise, can be an array containing a
+ /// left and a right delimiter.
+ ///
+ /// ```example
+ /// #set math.mat(delim: "[")
+ /// $ mat(1, 2; 3, 4) $
+ /// ```
+ #[default(DelimiterPair::PAREN)]
+ pub delim: DelimiterPair,
+
+ /// The horizontal alignment that each cell should have.
+ ///
+ /// ```example
+ /// #set math.mat(align: right)
+ /// $ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $
+ /// ```
+ #[resolve]
+ #[default(HAlignment::Center)]
+ pub align: HAlignment,
+
+ /// Draws augmentation lines in a matrix.
+ ///
+ /// - `{none}`: No lines are drawn.
+ /// - A single number: A vertical augmentation line is drawn
+ /// after the specified column number. Negative numbers start from the end.
+ /// - A dictionary: With a dictionary, multiple augmentation lines can be
+ /// drawn both horizontally and vertically. Additionally, the style of the
+ /// lines can be set. The dictionary can contain the following keys:
+ /// - `hline`: The offsets at which horizontal lines should be drawn.
+ /// For example, an offset of `2` would result in a horizontal line
+ /// being drawn after the second row of the matrix. Accepts either an
+ /// integer for a single line, or an array of integers
+ /// for multiple lines. Like for a single number, negative numbers start from the end.
+ /// - `vline`: The offsets at which vertical lines should be drawn.
+ /// For example, an offset of `2` would result in a vertical line being
+ /// drawn after the second column of the matrix. Accepts either an
+ /// integer for a single line, or an array of integers
+ /// for multiple lines. Like for a single number, negative numbers start from the end.
+ /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`,
+ /// takes on a thickness of 0.05em and square line caps.
+ ///
+ /// ```example
+ /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $
+ /// // Equivalent to:
+ /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $
+ /// ```
+ ///
+ /// ```example
+ /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $
+ /// ```
+ #[resolve]
+ #[fold]
+ pub augment: Option<Augment>,
+
+ /// The gap between rows and columns.
+ ///
+ /// This is a shorthand to set `row-gap` and `column-gap` to the same value.
+ ///
+ /// ```example
+ /// #set math.mat(gap: 1em)
+ /// $ mat(1, 2; 3, 4) $
+ /// ```
+ #[external]
+ pub gap: Rel<Length>,
+
+ /// The gap between rows.
+ ///
+ /// ```example
+ /// #set math.mat(row-gap: 1em)
+ /// $ mat(1, 2; 3, 4) $
+ /// ```
+ #[parse(
+ let gap = args.named("gap")?;
+ args.named("row-gap")?.or(gap)
+ )]
+ #[default(DEFAULT_ROW_GAP.into())]
+ pub row_gap: Rel<Length>,
+
+ /// The gap between columns.
+ ///
+ /// ```example
+ /// #set math.mat(column-gap: 1em)
+ /// $ mat(1, 2; 3, 4) $
+ /// ```
+ #[parse(args.named("column-gap")?.or(gap))]
+ #[default(DEFAULT_COL_GAP.into())]
+ pub column_gap: Rel<Length>,
+
+ /// An array of arrays with the rows of the matrix.
+ ///
+ /// ```example
+ /// #let data = ((1, 2, 3), (4, 5, 6))
+ /// #let matrix = math.mat(..data)
+ /// $ v := matrix $
+ /// ```
+ #[variadic]
+ #[parse(
+ let mut rows = vec![];
+ let mut width = 0;
+
+ let values = args.all::<Spanned<Value>>()?;
+ if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) {
+ for Spanned { v, span } in values {
+ let array = v.cast::<Array>().at(span)?;
+ let row: Vec<_> = array.into_iter().map(Value::display).collect();
+ width = width.max(row.len());
+ rows.push(row);
+ }
+ } else {
+ rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()];
+ }
+
+ for row in &mut rows {
+ if row.len() < width {
+ row.resize(width, Content::empty());
+ }
+ }
+
+ rows
+ )]
+ pub rows: Vec<Vec<Content>>,
+}
+
+/// A case distinction.
+///
+/// Content across different branches can be aligned with the `&` symbol.
+///
+/// # Example
+/// ```example
+/// $ f(x, y) := cases(
+/// 1 "if" (x dot y)/2 <= 0,
+/// 2 "if" x "is even",
+/// 3 "if" x in NN,
+/// 4 "else",
+/// ) $
+/// ```
+#[elem(Mathy)]
+pub struct CasesElem {
+ /// The delimiter to use.
+ ///
+ /// Can be a single character specifying the left delimiter, in which case
+ /// the right delimiter is inferred. Otherwise, can be an array containing a
+ /// left and a right delimiter.
+ ///
+ /// ```example
+ /// #set math.cases(delim: "[")
+ /// $ x = cases(1, 2) $
+ /// ```
+ #[default(DelimiterPair::BRACE)]
+ pub delim: DelimiterPair,
+
+ /// Whether the direction of cases should be reversed.
+ ///
+ /// ```example
+ /// #set math.cases(reverse: true)
+ /// $ cases(1, 2) = x $
+ /// ```
+ #[default(false)]
+ pub reverse: bool,
+
+ /// The gap between branches.
+ ///
+ /// ```example
+ /// #set math.cases(gap: 1em)
+ /// $ x = cases(1, 2) $
+ /// ```
+ #[default(DEFAULT_ROW_GAP.into())]
+ pub gap: Rel<Length>,
+
+ /// The branches of the case distinction.
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+/// A delimiter is a single character that is used to delimit a matrix, vector
+/// or cases. The character has to be a Unicode codepoint tagged as a math
+/// "opening", "closing" or "fence".
+///
+/// Typically, the delimiter is stretched to fit the height of whatever it
+/// delimits.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Delimiter(Option<char>);
+
+cast! {
+ Delimiter,
+ self => self.0.into_value(),
+ _: NoneValue => Self::none(),
+ v: Symbol => Self::char(v.get())?,
+ v: char => Self::char(v)?,
+}
+
+impl Delimiter {
+ pub fn none() -> Self {
+ Self(None)
+ }
+
+ pub fn char(c: char) -> StrResult<Self> {
+ if !matches!(
+ unicode_math_class::class(c),
+ Some(MathClass::Opening | MathClass::Closing | MathClass::Fence),
+ ) {
+ bail!("invalid delimiter: \"{}\"", c)
+ }
+ Ok(Self(Some(c)))
+ }
+
+ pub fn get(self) -> Option<char> {
+ self.0
+ }
+
+ pub fn find_matching(self) -> Self {
+ match self.0 {
+ None => Self::none(),
+ Some('[') => Self(Some(']')),
+ Some(']') => Self(Some('[')),
+ Some('{') => Self(Some('}')),
+ Some('}') => Self(Some('{')),
+ Some(c) => match unicode_math_class::class(c) {
+ Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)),
+ Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)),
+ _ => Self(Some(c)),
+ },
+ }
+ }
+}
+
+/// A pair of delimiters (one closing, one opening) used for matrices, vectors
+/// and cases.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct DelimiterPair {
+ open: Delimiter,
+ close: Delimiter,
+}
+
+cast! {
+ DelimiterPair,
+
+ self => array![self.open, self.close].into_value(),
+
+ v: Array => match v.as_slice() {
+ [open, close] => Self {
+ open: open.clone().cast()?,
+ close: close.clone().cast()?,
+ },
+ _ => bail!("expected 2 delimiters, found {}", v.len())
+ },
+ v: Delimiter => Self { open: v, close: v.find_matching() }
+}
+
+impl DelimiterPair {
+ const PAREN: Self = Self {
+ open: Delimiter(Some('(')),
+ close: Delimiter(Some(')')),
+ };
+ const BRACE: Self = Self {
+ open: Delimiter(Some('{')),
+ close: Delimiter(Some('}')),
+ };
+
+ /// The delimiter's opening character.
+ pub fn open(self) -> Option<char> {
+ self.open.get()
+ }
+
+ /// The delimiter's closing character.
+ pub fn close(self) -> Option<char> {
+ self.close.get()
+ }
+}
+
+/// Parameters specifying how augmentation lines
+/// should be drawn on a matrix.
+#[derive(Debug, Default, Clone, PartialEq, Hash)]
+pub struct Augment<T: Numeric = Length> {
+ pub hline: AugmentOffsets,
+ pub vline: AugmentOffsets,
+ pub stroke: Smart<Stroke<T>>,
+}
+
+impl<T: Numeric + Fold> Fold for Augment<T> {
+ fn fold(self, outer: Self) -> Self {
+ Self {
+ stroke: match (self.stroke, outer.stroke) {
+ (Smart::Custom(inner), Smart::Custom(outer)) => {
+ Smart::Custom(inner.fold(outer))
+ }
+ // Usually, folding an inner `auto` with an `outer` prefers
+ // the explicit `auto`. However, here `auto` means unspecified
+ // and thus we want `outer`.
+ (inner, outer) => inner.or(outer),
+ },
+ ..self
+ }
+ }
+}
+
+impl Resolve for Augment {
+ type Output = Augment<Abs>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ Augment {
+ hline: self.hline,
+ vline: self.vline,
+ stroke: self.stroke.resolve(styles),
+ }
+ }
+}
+
+cast! {
+ Augment,
+ self => {
+ // if the stroke is auto and there is only one vertical line,
+ if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 {
+ return self.vline.0[0].into_value();
+ }
+
+ dict! {
+ "hline" => self.hline,
+ "vline" => self.vline,
+ "stroke" => self.stroke,
+ }.into_value()
+ },
+ v: isize => Augment {
+ hline: AugmentOffsets::default(),
+ vline: AugmentOffsets(smallvec![v]),
+ stroke: Smart::Auto,
+ },
+ mut dict: Dict => {
+ let mut take = |key| dict.take(key).ok().map(AugmentOffsets::from_value).transpose();
+ let hline = take("hline")?.unwrap_or_default();
+ let vline = take("vline")?.unwrap_or_default();
+ let stroke = dict.take("stroke")
+ .ok()
+ .map(Stroke::from_value)
+ .transpose()?
+ .map(Smart::Custom)
+ .unwrap_or(Smart::Auto);
+ Augment { hline, vline, stroke }
+ },
+}
+
+cast! {
+ Augment<Abs>,
+ self => self.into_value(),
+}
+
+/// The offsets at which augmentation lines should be drawn on a matrix.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct AugmentOffsets(pub SmallVec<[isize; 1]>);
+
+cast! {
+ AugmentOffsets,
+ self => self.0.into_value(),
+ v: isize => Self(smallvec![v]),
+ v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
+}
diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs
new file mode 100644
index 00000000..53b1f072
--- /dev/null
+++ b/crates/typst-library/src/math/mod.rs
@@ -0,0 +1,257 @@
+//! Mathematical formulas.
+
+pub mod accent;
+mod attach;
+mod cancel;
+mod equation;
+mod frac;
+mod lr;
+mod matrix;
+mod op;
+mod root;
+mod style;
+mod underover;
+
+pub use self::accent::{Accent, AccentElem};
+pub use self::attach::*;
+pub use self::cancel::*;
+pub use self::equation::*;
+pub use self::frac::*;
+pub use self::lr::*;
+pub use self::matrix::*;
+pub use self::op::*;
+pub use self::root::*;
+pub use self::style::*;
+pub use self::underover::*;
+
+use typst_utils::singleton;
+use unicode_math_class::MathClass;
+
+use crate::foundations::{
+ category, elem, Category, Content, Module, NativeElement, Scope,
+};
+use crate::layout::{Em, HElem};
+use crate::text::TextElem;
+
+/// Typst has special [syntax]($syntax/#math) and library functions to typeset
+/// mathematical formulas. Math formulas can be displayed inline with text or as
+/// separate blocks. They will be typeset into their own block if they start and
+/// end with at least one space (e.g. `[$ x^2 $]`).
+///
+/// # Variables
+/// In math, single letters are always displayed as is. Multiple letters,
+/// however, are interpreted as variables and functions. To display multiple
+/// letters verbatim, you can place them into quotes and to access single letter
+/// variables, you can use the [hash syntax]($scripting/#expressions).
+///
+/// ```example
+/// $ A = pi r^2 $
+/// $ "area" = pi dot "radius"^2 $
+/// $ cal(A) :=
+/// { x in RR | x "is natural" } $
+/// #let x = 5
+/// $ #x < 17 $
+/// ```
+///
+/// # Symbols
+/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like
+/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in
+/// different variants. You can select between different variants by applying
+/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of
+/// shorthand sequences like `=>` that approximate a symbol. When such a
+/// shorthand exists, the symbol's documentation lists it.
+///
+/// ```example
+/// $ x < y => x gt.eq.not y $
+/// ```
+///
+/// # Line Breaks
+/// Formulas can also contain line breaks. Each line can contain one or multiple
+/// _alignment points_ (`&`) which are then aligned.
+///
+/// ```example
+/// $ sum_(k=0)^n k
+/// &= 1 + ... + n \
+/// &= (n(n+1)) / 2 $
+/// ```
+///
+/// # Function calls
+/// Math mode supports special function calls without the hash prefix. In these
+/// "math calls", the argument list works a little differently than in code:
+///
+/// - Within them, Typst is still in "math mode". Thus, you can write math
+/// directly into them, but need to use hash syntax to pass code expressions
+/// (except for strings, which are available in the math syntax).
+/// - They support positional and named arguments, but don't support trailing
+/// content blocks and argument spreading.
+/// - They provide additional syntax for 2-dimensional argument lists. The
+/// semicolon (`;`) merges preceding arguments separated by commas into an
+/// array argument.
+///
+/// ```example
+/// $ frac(a^2, 2) $
+/// $ vec(1, 2, delim: "[") $
+/// $ mat(1, 2; 3, 4) $
+/// $ lim_x =
+/// op("lim", limits: #true)_x $
+/// ```
+///
+/// To write a verbatim comma or semicolon in a math call, escape it with a
+/// backslash. The colon on the other hand is only recognized in a special way
+/// if directly preceded by an identifier, so to display it verbatim in those
+/// cases, you can just insert a space before it.
+///
+/// Functions calls preceded by a hash are normal code function calls and not
+/// affected by these rules.
+///
+/// # Alignment
+/// When equations include multiple _alignment points_ (`&`), this creates
+/// blocks of alternatingly right- and left-aligned columns. In the example
+/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is
+/// left-aligned. The word "given" is also left-aligned because `&&` creates two
+/// alignment points in a row, alternating the alignment twice. `& &` and `&&`
+/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned
+/// because just one `&` precedes it. Each alignment point simply alternates
+/// between right-aligned/left-aligned.
+///
+/// ```example
+/// $ (3x + y) / 7 &= 9 && "given" \
+/// 3x + y &= 63 & "multiply by 7" \
+/// 3x &= 63 - y && "subtract y" \
+/// x &= 21 - y/3 & "divide by 3" $
+/// ```
+///
+/// # Math fonts
+/// You can set the math font by with a [show-set rule]($styling/#show-rules) as
+/// demonstrated below. Note that only special OpenType math fonts are suitable
+/// for typesetting maths.
+///
+/// ```example
+/// #show math.equation: set text(font: "Fira Math")
+/// $ sum_(i in NN) 1 + i $
+/// ```
+///
+/// # Math module
+/// All math functions are part of the `math` [module]($scripting/#modules),
+/// which is available by default in equations. Outside of equations, they can
+/// be accessed with the `math.` prefix.
+#[category]
+pub static MATH: Category;
+
+// Spacings.
+pub const THIN: Em = Em::new(1.0 / 6.0);
+pub const MEDIUM: Em = Em::new(2.0 / 9.0);
+pub const THICK: Em = Em::new(5.0 / 18.0);
+pub const QUAD: Em = Em::new(1.0);
+pub const WIDE: Em = Em::new(2.0);
+
+/// Create a module with all math definitions.
+pub fn module() -> Module {
+ let mut math = Scope::deduplicating();
+ math.category(MATH);
+ math.define_elem::<EquationElem>();
+ math.define_elem::<TextElem>();
+ math.define_elem::<LrElem>();
+ math.define_elem::<MidElem>();
+ math.define_elem::<AttachElem>();
+ math.define_elem::<StretchElem>();
+ math.define_elem::<ScriptsElem>();
+ math.define_elem::<LimitsElem>();
+ math.define_elem::<AccentElem>();
+ math.define_elem::<UnderlineElem>();
+ math.define_elem::<OverlineElem>();
+ math.define_elem::<UnderbraceElem>();
+ math.define_elem::<OverbraceElem>();
+ math.define_elem::<UnderbracketElem>();
+ math.define_elem::<OverbracketElem>();
+ math.define_elem::<UnderparenElem>();
+ math.define_elem::<OverparenElem>();
+ math.define_elem::<UndershellElem>();
+ math.define_elem::<OvershellElem>();
+ math.define_elem::<CancelElem>();
+ math.define_elem::<FracElem>();
+ math.define_elem::<BinomElem>();
+ math.define_elem::<VecElem>();
+ math.define_elem::<MatElem>();
+ math.define_elem::<CasesElem>();
+ math.define_elem::<RootElem>();
+ math.define_elem::<ClassElem>();
+ math.define_elem::<OpElem>();
+ math.define_elem::<PrimesElem>();
+ math.define_func::<abs>();
+ math.define_func::<norm>();
+ math.define_func::<round>();
+ math.define_func::<sqrt>();
+ math.define_func::<upright>();
+ math.define_func::<bold>();
+ math.define_func::<italic>();
+ math.define_func::<serif>();
+ math.define_func::<sans>();
+ math.define_func::<cal>();
+ math.define_func::<frak>();
+ math.define_func::<mono>();
+ math.define_func::<bb>();
+ math.define_func::<display>();
+ math.define_func::<inline>();
+ math.define_func::<script>();
+ math.define_func::<sscript>();
+
+ // Text operators.
+ op::define(&mut math);
+
+ // Spacings.
+ math.define("thin", HElem::new(THIN.into()).pack());
+ math.define("med", HElem::new(MEDIUM.into()).pack());
+ math.define("thick", HElem::new(THICK.into()).pack());
+ math.define("quad", HElem::new(QUAD.into()).pack());
+ math.define("wide", HElem::new(WIDE.into()).pack());
+
+ // Symbols.
+ for (name, symbol) in crate::symbols::SYM {
+ math.define(*name, symbol.clone());
+ }
+
+ Module::new("math", math)
+}
+
+/// Trait for recognizing math elements and auto-wrapping them in equations.
+pub trait Mathy {}
+
+/// A math alignment point: `&`, `&&`.
+#[elem(title = "Alignment Point", Mathy)]
+pub struct AlignPointElem {}
+
+impl AlignPointElem {
+ /// Get the globally shared alignment point element.
+ pub fn shared() -> &'static Content {
+ singleton!(Content, AlignPointElem::new().pack())
+ }
+}
+
+/// Forced use of a certain math class.
+///
+/// This is useful to treat certain symbols as if they were of a different
+/// class, e.g. to make a symbol behave like a relation. The class of a symbol
+/// defines the way it is laid out, including spacing around it, and how its
+/// scripts are attached by default. Note that the latter can always be
+/// overridden using [`{limits}`](math.limits) and [`{scripts}`](math.scripts).
+///
+/// # Example
+/// ```example
+/// #let loves = math.class(
+/// "relation",
+/// sym.suit.heart,
+/// )
+///
+/// $x loves y and y loves 5$
+/// ```
+#[elem(Mathy)]
+pub struct ClassElem {
+ /// The class to apply to the content.
+ #[required]
+ pub class: MathClass,
+
+ /// The content to which the class is applied.
+ #[required]
+ pub body: Content,
+}
diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs
new file mode 100644
index 00000000..ef24705a
--- /dev/null
+++ b/crates/typst-library/src/math/op.rs
@@ -0,0 +1,103 @@
+use ecow::EcoString;
+
+use crate::foundations::{elem, Content, NativeElement, Scope};
+use crate::layout::HElem;
+use crate::math::{upright, Mathy, THIN};
+use crate::text::TextElem;
+
+/// A text operator in an equation.
+///
+/// # Example
+/// ```example
+/// $ tan x = (sin x)/(cos x) $
+/// $ op("custom",
+/// limits: #true)_(n->oo) n $
+/// ```
+///
+/// # Predefined Operators { #predefined }
+/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
+/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
+/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
+/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
+/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
+#[elem(title = "Text Operator", Mathy)]
+pub struct OpElem {
+ /// The operator's text.
+ #[required]
+ pub text: Content,
+
+ /// Whether the operator should show attachments as limits in display mode.
+ #[default(false)]
+ pub limits: bool,
+}
+
+macro_rules! ops {
+ ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => {
+ pub(super) fn define(math: &mut Scope) {
+ $({
+ let operator = EcoString::from(ops!(@name $name $(: $value)?));
+ math.define(
+ stringify!($name),
+ OpElem::new(TextElem::new(operator).into())
+ .with_limits(ops!(@limit $($tts)*))
+ .pack()
+ );
+ })*
+
+ let dif = |d| {
+ HElem::new(THIN.into()).with_weak(true).pack()
+ + upright(TextElem::packed(d))
+ };
+ math.define("dif", dif('d'));
+ math.define("Dif", dif('D'));
+ }
+ };
+ (@name $name:ident) => { stringify!($name) };
+ (@name $name:ident: $value:literal) => { $value };
+ (@limit limits) => { true };
+ (@limit) => { false };
+}
+
+ops! {
+ arccos,
+ arcsin,
+ arctan,
+ arg,
+ cos,
+ cosh,
+ cot,
+ coth,
+ csc,
+ csch,
+ ctg,
+ deg,
+ det (limits),
+ dim,
+ exp,
+ gcd (limits),
+ hom,
+ id,
+ im,
+ inf (limits),
+ ker,
+ lg,
+ lim (limits),
+ liminf: "lim inf" (limits),
+ limsup: "lim sup" (limits),
+ ln,
+ log,
+ max (limits),
+ min (limits),
+ mod,
+ Pr (limits),
+ sec,
+ sech,
+ sin,
+ sinc,
+ sinh,
+ sup (limits),
+ tan,
+ tanh,
+ tg,
+ tr,
+}
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
new file mode 100644
index 00000000..e25c6d42
--- /dev/null
+++ b/crates/typst-library/src/math/root.rs
@@ -0,0 +1,35 @@
+use typst_syntax::Span;
+
+use crate::foundations::{elem, func, Content, NativeElement};
+use crate::math::Mathy;
+
+/// A square root.
+///
+/// ```example
+/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
+/// ```
+#[func(title = "Square Root")]
+pub fn sqrt(
+ /// The call span of this function.
+ span: Span,
+ /// The expression to take the square root of.
+ radicand: Content,
+) -> Content {
+ RootElem::new(radicand).pack().spanned(span)
+}
+
+/// A general root.
+///
+/// ```example
+/// $ root(3, x) $
+/// ```
+#[elem(Mathy)]
+pub struct RootElem {
+ /// Which root of the radicand to take.
+ #[positional]
+ pub index: Option<Content>,
+
+ /// The expression to take the root of.
+ #[required]
+ pub radicand: Content,
+}
diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs
new file mode 100644
index 00000000..f3d28f2a
--- /dev/null
+++ b/crates/typst-library/src/math/style.rs
@@ -0,0 +1,254 @@
+use crate::foundations::{func, Cast, Content, Smart};
+use crate::math::EquationElem;
+
+/// Bold font style in math.
+///
+/// ```example
+/// $ bold(A) := B^+ $
+/// ```
+#[func(keywords = ["mathbf"])]
+pub fn bold(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_bold(true))
+}
+
+/// Upright (non-italic) font style in math.
+///
+/// ```example
+/// $ upright(A) != A $
+/// ```
+#[func(keywords = ["mathup"])]
+pub fn upright(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_italic(Smart::Custom(false)))
+}
+
+/// Italic font style in math.
+///
+/// For roman letters and greek lowercase letters, this is already the default.
+#[func(keywords = ["mathit"])]
+pub fn italic(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_italic(Smart::Custom(true)))
+}
+
+/// Serif (roman) font style in math.
+///
+/// This is already the default.
+#[func(keywords = ["mathrm"])]
+pub fn serif(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Serif))
+}
+
+/// Sans-serif font style in math.
+///
+/// ```example
+/// $ sans(A B C) $
+/// ```
+#[func(title = "Sans Serif", keywords = ["mathsf"])]
+pub fn sans(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Sans))
+}
+
+/// Calligraphic font style in math.
+///
+/// ```example
+/// Let $cal(P)$ be the set of ...
+/// ```
+///
+/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these
+/// styles share the same Unicode codepoints. Switching between the styles is
+/// thus only possible if supported by the font via
+/// [font features]($text.features).
+///
+/// For the default math font, the roundhand style is available through the
+/// `ss01` feature. Therefore, you could define your own version of `\mathscr`
+/// like this:
+///
+/// ```example
+/// #let scr(it) = text(
+/// features: ("ss01",),
+/// box($cal(it)$),
+/// )
+///
+/// We establish $cal(P) != scr(P)$.
+/// ```
+///
+/// (The box is not conceptually necessary, but unfortunately currently needed
+/// due to limitations in Typst's text style handling in math.)
+#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])]
+pub fn cal(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Cal))
+}
+
+/// Fraktur font style in math.
+///
+/// ```example
+/// $ frak(P) $
+/// ```
+#[func(title = "Fraktur", keywords = ["mathfrak"])]
+pub fn frak(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Frak))
+}
+
+/// Monospace font style in math.
+///
+/// ```example
+/// $ mono(x + y = z) $
+/// ```
+#[func(title = "Monospace", keywords = ["mathtt"])]
+pub fn mono(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Mono))
+}
+
+/// Blackboard bold (double-struck) font style in math.
+///
+/// For uppercase latin letters, blackboard bold is additionally available
+/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`.
+///
+/// ```example
+/// $ bb(b) $
+/// $ bb(N) = NN $
+/// $ f: NN -> RR $
+/// ```
+#[func(title = "Blackboard Bold", keywords = ["mathbb"])]
+pub fn bb(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ body.styled(EquationElem::set_variant(MathVariant::Bb))
+}
+
+/// Forced display style in math.
+///
+/// This is the normal size for block equations.
+///
+/// ```example
+/// $sum_i x_i/2 = display(sum_i x_i/2)$
+/// ```
+#[func(title = "Display Size", keywords = ["displaystyle"])]
+pub fn display(
+ /// The content to size.
+ body: Content,
+ /// Whether to impose a height restriction for exponents, like regular sub-
+ /// and superscripts do.
+ #[named]
+ #[default(false)]
+ cramped: bool,
+) -> Content {
+ body.styled(EquationElem::set_size(MathSize::Display))
+ .styled(EquationElem::set_cramped(cramped))
+}
+
+/// Forced inline (text) style in math.
+///
+/// This is the normal size for inline equations.
+///
+/// ```example
+/// $ sum_i x_i/2
+/// = inline(sum_i x_i/2) $
+/// ```
+#[func(title = "Inline Size", keywords = ["textstyle"])]
+pub fn inline(
+ /// The content to size.
+ body: Content,
+ /// Whether to impose a height restriction for exponents, like regular sub-
+ /// and superscripts do.
+ #[named]
+ #[default(false)]
+ cramped: bool,
+) -> Content {
+ body.styled(EquationElem::set_size(MathSize::Text))
+ .styled(EquationElem::set_cramped(cramped))
+}
+
+/// Forced script style in math.
+///
+/// This is the smaller size used in powers or sub- or superscripts.
+///
+/// ```example
+/// $sum_i x_i/2 = script(sum_i x_i/2)$
+/// ```
+#[func(title = "Script Size", keywords = ["scriptstyle"])]
+pub fn script(
+ /// The content to size.
+ body: Content,
+ /// Whether to impose a height restriction for exponents, like regular sub-
+ /// and superscripts do.
+ #[named]
+ #[default(true)]
+ cramped: bool,
+) -> Content {
+ body.styled(EquationElem::set_size(MathSize::Script))
+ .styled(EquationElem::set_cramped(cramped))
+}
+
+/// Forced second script style in math.
+///
+/// This is the smallest size, used in second-level sub- and superscripts
+/// (script of the script).
+///
+/// ```example
+/// $sum_i x_i/2 = sscript(sum_i x_i/2)$
+/// ```
+#[func(title = "Script-Script Size", keywords = ["scriptscriptstyle"])]
+pub fn sscript(
+ /// The content to size.
+ body: Content,
+ /// Whether to impose a height restriction for exponents, like regular sub-
+ /// and superscripts do.
+ #[named]
+ #[default(true)]
+ cramped: bool,
+) -> Content {
+ body.styled(EquationElem::set_size(MathSize::ScriptScript))
+ .styled(EquationElem::set_cramped(cramped))
+}
+
+/// The size of elements in an equation.
+///
+/// See the TeXbook p. 141.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)]
+pub enum MathSize {
+ /// Second-level sub- and superscripts.
+ ScriptScript,
+ /// Sub- and superscripts.
+ Script,
+ /// Math in text.
+ Text,
+ /// Math on its own line.
+ Display,
+}
+
+/// A mathematical style variant, as defined by Unicode.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)]
+pub enum MathVariant {
+ #[default]
+ Serif,
+ Sans,
+ Cal,
+ Frak,
+ Mono,
+ Bb,
+}
diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs
new file mode 100644
index 00000000..302c51af
--- /dev/null
+++ b/crates/typst-library/src/math/underover.rs
@@ -0,0 +1,156 @@
+use crate::foundations::{elem, Content};
+use crate::math::Mathy;
+
+/// A horizontal line under content.
+///
+/// ```example
+/// $ underline(1 + 2 + ... + 5) $
+/// ```
+#[elem(Mathy)]
+pub struct UnderlineElem {
+ /// The content above the line.
+ #[required]
+ pub body: Content,
+}
+
+/// A horizontal line over content.
+///
+/// ```example
+/// $ overline(1 + 2 + ... + 5) $
+/// ```
+#[elem(Mathy)]
+pub struct OverlineElem {
+ /// The content below the line.
+ #[required]
+ pub body: Content,
+}
+
+/// A horizontal brace under content, with an optional annotation below.
+///
+/// ```example
+/// $ underbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct UnderbraceElem {
+ /// The content above the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal brace over content, with an optional annotation above.
+///
+/// ```example
+/// $ overbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct OverbraceElem {
+ /// The content below the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal bracket under content, with an optional annotation below.
+///
+/// ```example
+/// $ underbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct UnderbracketElem {
+ /// The content above the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal bracket over content, with an optional annotation above.
+///
+/// ```example
+/// $ overbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct OverbracketElem {
+ /// The content below the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal parenthesis under content, with an optional annotation below.
+///
+/// ```example
+/// $ underparen(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct UnderparenElem {
+ /// The content above the parenthesis.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the parenthesis.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal parenthesis over content, with an optional annotation above.
+///
+/// ```example
+/// $ overparen(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct OverparenElem {
+ /// The content below the parenthesis.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the parenthesis.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal tortoise shell bracket under content, with an optional
+/// annotation below.
+///
+/// ```example
+/// $ undershell(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct UndershellElem {
+ /// The content above the tortoise shell bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the tortoise shell bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+/// A horizontal tortoise shell bracket over content, with an optional
+/// annotation above.
+///
+/// ```example
+/// $ overshell(1 + 2 + ... + 5, "numbers") $
+/// ```
+#[elem(Mathy)]
+pub struct OvershellElem {
+ /// The content below the tortoise shell bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the tortoise shell bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}