summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math
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
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/math')
-rw-r--r--crates/typst-library/src/math/accent.rs139
-rw-r--r--crates/typst-library/src/math/align.rs63
-rw-r--r--crates/typst-library/src/math/attach.rs411
-rw-r--r--crates/typst-library/src/math/cancel.rs187
-rw-r--r--crates/typst-library/src/math/ctx.rs268
-rw-r--r--crates/typst-library/src/math/delimited.rs200
-rw-r--r--crates/typst-library/src/math/frac.rs148
-rw-r--r--crates/typst-library/src/math/fragment.rs414
-rw-r--r--crates/typst-library/src/math/matrix.rs313
-rw-r--r--crates/typst-library/src/math/mod.rs480
-rw-r--r--crates/typst-library/src/math/op.rs113
-rw-r--r--crates/typst-library/src/math/root.rs156
-rw-r--r--crates/typst-library/src/math/row.rs258
-rw-r--r--crates/typst-library/src/math/spacing.rs60
-rw-r--r--crates/typst-library/src/math/stretch.rs199
-rw-r--r--crates/typst-library/src/math/style.rs620
-rw-r--r--crates/typst-library/src/math/underover.rs339
17 files changed, 4368 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..d1bee198
--- /dev/null
+++ b/crates/typst-library/src/math/accent.rs
@@ -0,0 +1,139 @@
+use super::*;
+
+/// How much the accent can be shorter than the base.
+const ACCENT_SHORT_FALL: Em = Em::new(0.5);
+
+/// Attaches an accent to a base.
+///
+/// ## Example { #example }
+/// ```example
+/// $grave(a) = accent(a, `)$ \
+/// $arrow(a) = accent(a, arrow)$ \
+/// $tilde(a) = accent(a, \u{0303})$
+/// ```
+///
+/// Display: Accent
+/// Category: math
+#[element(LayoutMath)]
+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` | `¯` |
+ /// | Breve | `breve` | `˘` |
+ /// | Dot | `dot` | `.` |
+ /// | Double dot | `dot.double` | `¨` |
+ /// | Triple dot | `dot.triple` | <code>&tdot;</code> |
+ /// | Quadruple dot | `dot.quad` | <code>&DotDot;</code> |
+ /// | Diaeresis | `diaer` | `¨` |
+ /// | Circle | `circle` | `∘` |
+ /// | Double acute | `acute.double` | `˝` |
+ /// | Caron | `caron` | `ˇ` |
+ /// | Right arrow | `arrow`, `->` | `→` |
+ /// | Left arrow | `arrow.l`, `<-` | `←` |
+ #[required]
+ pub accent: Accent,
+}
+
+impl LayoutMath for AccentElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ ctx.style(ctx.style.with_cramped(true));
+ let base = ctx.layout_fragment(&self.base())?;
+ ctx.unstyle();
+
+ // Preserve class to preserve automatic spacing.
+ let base_class = base.class().unwrap_or(MathClass::Normal);
+ let base_attach = match &base {
+ MathFragment::Glyph(base) => {
+ attachment(ctx, base.id, base.italics_correction)
+ }
+ _ => (base.width() + base.italics_correction()) / 2.0,
+ };
+
+ // Forcing the accent to be at least as large as the base makes it too
+ // wide in many case.
+ let Accent(c) = self.accent();
+ let glyph = GlyphFragment::new(ctx, c, self.span());
+ let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
+ let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall);
+ let accent = variant.frame;
+ let accent_attach = match variant.id {
+ Some(id) => attachment(ctx, id, variant.italics_correction),
+ None => accent.width() / 2.0,
+ };
+
+ // Descent is negative because the accent's ink bottom is above the
+ // baseline. Therefore, the default gap is the accent's negated descent
+ // minus the accent base height. Only if the base is very small, we need
+ // a larger gap so that the accent doesn't move too low.
+ let accent_base_height = scaled!(ctx, accent_base_height);
+ let gap = -accent.descent() - base.height().min(accent_base_height);
+ let size = Size::new(base.width(), accent.height() + gap + base.height());
+ let accent_pos = Point::with_x(base_attach - accent_attach);
+ let base_pos = Point::with_y(accent.height() + gap);
+ let base_ascent = base.ascent();
+ let baseline = base_pos.y + base.ascent();
+
+ let mut frame = Frame::new(size);
+ frame.set_baseline(baseline);
+ frame.push_frame(accent_pos, accent);
+ frame.push_frame(base_pos, base.into_frame());
+ ctx.push(
+ FrameFragment::new(ctx, frame)
+ .with_class(base_class)
+ .with_base_ascent(base_ascent),
+ );
+
+ Ok(())
+ }
+}
+
+/// The horizontal attachment position for the given glyph.
+fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs {
+ ctx.table
+ .glyph_info
+ .and_then(|info| info.top_accent_attachments)
+ .and_then(|attachments| attachments.get(id))
+ .map(|record| record.value.scaled(ctx))
+ .unwrap_or_else(|| {
+ let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
+ (advance.scaled(ctx) + italics_correction) / 2.0
+ })
+}
+
+/// An accent character.
+pub struct Accent(char);
+
+impl Accent {
+ /// Normalize a character into an accent.
+ pub fn new(c: char) -> Self {
+ Self(Symbol::combining_accent(c).unwrap_or(c))
+ }
+}
+
+cast! {
+ Accent,
+ self => self.0.into_value(),
+ v: char => Self::new(v),
+ v: Content => match v.to::<TextElem>() {
+ Some(elem) => Value::Str(elem.text().into()).cast()?,
+ None => bail!("expected text"),
+ },
+}
diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs
new file mode 100644
index 00000000..aee89a89
--- /dev/null
+++ b/crates/typst-library/src/math/align.rs
@@ -0,0 +1,63 @@
+use super::*;
+
+/// A math alignment point: `&`, `&&`.
+///
+/// Display: Alignment Point
+/// Category: math
+#[element(LayoutMath)]
+pub struct AlignPointElem {}
+
+impl LayoutMath for AlignPointElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ ctx.push(MathFragment::Align);
+ Ok(())
+ }
+}
+
+pub(super) struct AlignmentResult {
+ pub points: Vec<Abs>,
+ pub width: Abs,
+}
+
+/// Determine the position of the alignment points.
+pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult {
+ let mut widths = Vec::<Abs>::new();
+
+ let mut pending_width = Abs::zero();
+ for row in rows {
+ let mut width = Abs::zero();
+ let mut alignment_index = 0;
+
+ for fragment in row.iter() {
+ if matches!(fragment, MathFragment::Align) {
+ if alignment_index < widths.len() {
+ widths[alignment_index].set_max(width);
+ } else {
+ widths.push(width.max(pending_width));
+ }
+ width = Abs::zero();
+ alignment_index += 1;
+ } else {
+ width += fragment.width();
+ }
+ }
+ if widths.is_empty() {
+ pending_width.set_max(width);
+ } else if alignment_index < widths.len() {
+ widths[alignment_index].set_max(width);
+ } else {
+ widths.push(width.max(pending_width));
+ }
+ }
+
+ let mut points = widths;
+ for i in 1..points.len() {
+ let prev = points[i - 1];
+ points[i] += prev;
+ }
+ AlignmentResult {
+ width: points.last().copied().unwrap_or(pending_width),
+ points,
+ }
+}
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
new file mode 100644
index 00000000..fedeb908
--- /dev/null
+++ b/crates/typst-library/src/math/attach.rs
@@ -0,0 +1,411 @@
+use super::*;
+
+/// A base with optional attachments.
+///
+/// ## Example { #example }
+/// ```example
+/// // With syntax.
+/// $ sum_(i=0)^n a_i = 2^(1+i) $
+///
+/// // With function call.
+/// $ attach(
+/// Pi, t: alpha, b: beta,
+/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
+/// ) $
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax for attachments after the base: Use
+/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the
+/// hat (`^`) to indicate a superscript i.e. top attachment.
+///
+/// Display: Attachment
+/// Category: math
+#[element(LayoutMath)]
+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 LayoutMath for AttachElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>;
+ let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| {
+ getter(self, ctx.styles())
+ .map(|elem| ctx.layout_fragment(&elem))
+ .transpose()
+ };
+
+ let base = ctx.layout_fragment(&self.base())?;
+
+ ctx.style(ctx.style.for_superscript());
+ let tl = layout_attachment(ctx, Self::tl)?;
+ let tr = layout_attachment(ctx, Self::tr)?;
+ let t = layout_attachment(ctx, Self::t)?;
+ ctx.unstyle();
+
+ ctx.style(ctx.style.for_subscript());
+ let bl = layout_attachment(ctx, Self::bl)?;
+ let br = layout_attachment(ctx, Self::br)?;
+ let b = layout_attachment(ctx, Self::b)?;
+ ctx.unstyle();
+
+ let limits = base.limits().active(ctx);
+ let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) };
+ let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
+ layout_attachments(ctx, base, [tl, t, tr, bl, b, br])
+ }
+}
+
+/// Forces a base to display attachments as scripts.
+///
+/// ## Example { #example }
+/// ```example
+/// $ scripts(sum)_1^2 != sum_1^2 $
+/// ```
+///
+/// Display: Scripts
+/// Category: math
+#[element(LayoutMath)]
+pub struct ScriptsElem {
+ /// The base to attach the scripts to.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for ScriptsElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut fragment = ctx.layout_fragment(&self.body())?;
+ fragment.set_limits(Limits::Never);
+ ctx.push(fragment);
+ Ok(())
+ }
+}
+
+/// Forces a base to display attachments as limits.
+///
+/// ## Example { #example }
+/// ```example
+/// $ limits(A)_1^2 != A_1^2 $
+/// ```
+///
+/// Display: Limits
+/// Category: math
+#[element(LayoutMath)]
+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,
+}
+
+impl LayoutMath for LimitsElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut fragment = ctx.layout_fragment(&self.body())?;
+ fragment.set_limits(if self.inline(ctx.styles()) {
+ Limits::Always
+ } else {
+ Limits::Display
+ });
+ ctx.push(fragment);
+ Ok(())
+ }
+}
+
+/// Describes in which situation a frame should use limits for attachments.
+#[derive(Debug, Copy, Clone)]
+pub enum Limits {
+ /// Always scripts.
+ Never,
+ /// Display limits only in `display` math.
+ Display,
+ /// Always limits.
+ Always,
+}
+
+impl Limits {
+ /// The default limit configuration if the given character is the base.
+ pub fn for_char(c: char) -> Self {
+ if Self::DEFAULT_TO_LIMITS.contains(&c) {
+ Limits::Display
+ } else {
+ Limits::Never
+ }
+ }
+
+ /// Whether limits should be displayed in this context
+ pub fn active(&self, ctx: &MathContext) -> bool {
+ match self {
+ Self::Always => true,
+ Self::Display => ctx.style.size == MathSize::Display,
+ Self::Never => false,
+ }
+ }
+
+ /// Unicode codepoints that should show attachments as limits in display
+ /// mode.
+ #[rustfmt::skip]
+ const DEFAULT_TO_LIMITS: &[char] = &[
+ /* ∏ */ '\u{220F}', /* ∐ */ '\u{2210}', /* ∑ */ '\u{2211}',
+ /* ⋀ */ '\u{22C0}', /* ⋁ */ '\u{22C1}',
+ /* ⋂ */ '\u{22C2}', /* ⋃ */ '\u{22C3}',
+ /* ⨀ */ '\u{2A00}', /* ⨁ */ '\u{2A01}', /* ⨂ */ '\u{2A02}',
+ /* ⨃ */ '\u{2A03}', /* ⨄ */ '\u{2A04}',
+ /* ⨅ */ '\u{2A05}', /* ⨆ */ '\u{2A06}',
+ ];
+}
+
+macro_rules! measure {
+ ($e: ident, $attr: ident) => {
+ $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
+ };
+}
+
+/// Layout the attachments.
+fn layout_attachments(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
+) -> SourceResult<()> {
+ let (shift_up, shift_down) =
+ compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]);
+
+ let sup_delta = Abs::zero();
+ let sub_delta = -base.italics_correction();
+ let (base_width, base_ascent, base_descent) =
+ (base.width(), base.ascent(), base.descent());
+ let base_class = base.class().unwrap_or(MathClass::Normal);
+
+ let ascent = base_ascent
+ .max(shift_up + measure!(tr, ascent))
+ .max(shift_up + measure!(tl, ascent))
+ .max(shift_up + measure!(t, height));
+
+ let descent = base_descent
+ .max(shift_down + measure!(br, descent))
+ .max(shift_down + measure!(bl, descent))
+ .max(shift_down + measure!(b, height));
+
+ let pre_sup_width = measure!(tl, width);
+ let pre_sub_width = measure!(bl, width);
+ let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative.
+ let pre_width_max = pre_sup_width.max(pre_sub_width);
+ let post_max_width =
+ (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
+
+ let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b);
+ let base_pos =
+ Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset);
+ if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
+ ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class));
+ return Ok(());
+ }
+
+ let mut frame = Frame::new(Size::new(
+ pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script),
+ ascent + descent,
+ ));
+ frame.set_baseline(ascent);
+ frame.push_frame(base_pos, center_frame);
+
+ if let Some(tl) = tl {
+ let pos =
+ Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent());
+ frame.push_frame(pos, tl.into_frame());
+ }
+
+ if let Some(bl) = bl {
+ let pos =
+ Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent());
+ frame.push_frame(pos, bl.into_frame());
+ }
+
+ if let Some(tr) = tr {
+ let pos = Point::new(
+ sup_delta + pre_width_max + base_width,
+ ascent - shift_up - tr.ascent(),
+ );
+ frame.push_frame(pos, tr.into_frame());
+ }
+
+ if let Some(br) = br {
+ let pos = Point::new(
+ sub_delta + pre_width_max + base_width,
+ ascent + shift_down - br.ascent(),
+ );
+ frame.push_frame(pos, br.into_frame());
+ }
+
+ ctx.push(FrameFragment::new(ctx, frame).with_class(base_class));
+
+ Ok(())
+}
+
+fn attach_top_and_bottom(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ t: Option<MathFragment>,
+ b: Option<MathFragment>,
+) -> (Frame, Abs) {
+ let upper_gap_min = scaled!(ctx, upper_limit_gap_min);
+ let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min);
+ let lower_gap_min = scaled!(ctx, lower_limit_gap_min);
+ let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min);
+
+ let mut base_offset = Abs::zero();
+ let mut width = base.width();
+ let mut height = base.height();
+
+ if let Some(t) = &t {
+ let top_gap = upper_gap_min.max(upper_rise_min - t.descent());
+ width.set_max(t.width());
+ height += t.height() + top_gap;
+ base_offset = top_gap + t.height();
+ }
+
+ if let Some(b) = &b {
+ let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent());
+ width.set_max(b.width());
+ height += b.height() + bottom_gap;
+ }
+
+ let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
+ let delta = base.italics_correction() / 2.0;
+
+ let mut frame = Frame::new(Size::new(width, height));
+ frame.set_baseline(base_pos.y + base.ascent());
+ frame.push_frame(base_pos, base.into_frame());
+
+ if let Some(t) = t {
+ let top_pos = Point::with_x((width - t.width()) / 2.0 + delta);
+ frame.push_frame(top_pos, t.into_frame());
+ }
+
+ if let Some(b) = b {
+ let bottom_pos =
+ Point::new((width - b.width()) / 2.0 - delta, height - b.height());
+ frame.push_frame(bottom_pos, b.into_frame());
+ }
+
+ (frame, base_offset)
+}
+
+fn compute_shifts_up_and_down(
+ ctx: &MathContext,
+ base: &MathFragment,
+ [tl, tr, bl, br]: [&Option<MathFragment>; 4],
+) -> (Abs, Abs) {
+ let sup_shift_up = if ctx.style.cramped {
+ scaled!(ctx, superscript_shift_up_cramped)
+ } else {
+ scaled!(ctx, superscript_shift_up)
+ };
+
+ let sup_bottom_min = scaled!(ctx, superscript_bottom_min);
+ let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript);
+ let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max);
+ let gap_min = scaled!(ctx, sub_superscript_gap_min);
+ let sub_shift_down = scaled!(ctx, subscript_shift_down);
+ let sub_top_max = scaled!(ctx, subscript_top_max);
+ let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min);
+
+ let mut shift_up = Abs::zero();
+ let mut shift_down = Abs::zero();
+ let is_char_box = is_character_box(base);
+
+ if tl.is_some() || tr.is_some() {
+ let ascent = match &base {
+ MathFragment::Frame(frame) => frame.base_ascent,
+ _ => base.ascent(),
+ };
+ shift_up = shift_up
+ .max(sup_shift_up)
+ .max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max })
+ .max(sup_bottom_min + measure!(tl, descent))
+ .max(sup_bottom_min + measure!(tr, descent));
+ }
+
+ if bl.is_some() || br.is_some() {
+ shift_down = shift_down
+ .max(sub_shift_down)
+ .max(if is_char_box { Abs::zero() } else { base.descent() + sub_drop_min })
+ .max(measure!(bl, ascent) - sub_top_max)
+ .max(measure!(br, ascent) - sub_top_max);
+ }
+
+ for (sup, sub) in [(tl, bl), (tr, br)] {
+ if let (Some(sup), Some(sub)) = (&sup, &sub) {
+ let sup_bottom = shift_up - sup.descent();
+ let sub_top = sub.ascent() - shift_down;
+ let gap = sup_bottom - sub_top;
+ if gap >= gap_min {
+ continue;
+ }
+
+ let increase = gap_min - gap;
+ let sup_only =
+ (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
+ let rest = (increase - sup_only) / 2.0;
+ shift_up += sup_only + rest;
+ shift_down += rest;
+ }
+ }
+
+ (shift_up, shift_down)
+}
+
+/// Whether the fragment consists of a single character or atomic piece of text.
+fn is_character_box(fragment: &MathFragment) -> bool {
+ match fragment {
+ MathFragment::Glyph(_) | MathFragment::Variant(_) => {
+ fragment.class() != Some(MathClass::Large)
+ }
+ MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame),
+ _ => false,
+ }
+}
+
+/// Handles e.g. "sin", "log", "exp", "CustomOperator".
+fn is_atomic_text_frame(frame: &Frame) -> bool {
+ // Meta information isn't visible or renderable, so we exclude it.
+ let mut iter = frame
+ .items()
+ .map(|(_, item)| item)
+ .filter(|item| !matches!(item, FrameItem::Meta(_, _)));
+ matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none()
+}
diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs
new file mode 100644
index 00000000..f576a727
--- /dev/null
+++ b/crates/typst-library/src/math/cancel.rs
@@ -0,0 +1,187 @@
+use super::*;
+
+/// Displays a diagonal line over a part of an equation.
+///
+/// This is commonly used to show the elimination of a term.
+///
+/// ## Example { #example }
+/// ```example
+/// >>> #set page(width: 140pt)
+/// Here, we can simplify:
+/// $ (a dot b dot cancel(x)) /
+/// cancel(x) $
+/// ```
+///
+/// Display: Cancel
+/// Category: math
+#[element(LayoutMath)]
+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>,
+
+ /// If the cancel line should be inverted (pointing 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,
+
+ /// If 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 to rotate the cancel line. See the [line's
+ /// documentation]($func/line.angle) for more details.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ cancel(Pi, rotation: #30deg) $
+ /// ```
+ #[default(Angle::zero())]
+ pub rotation: Angle,
+
+ /// How to stroke the cancel line. See the
+ /// [line's documentation]($func/line.stroke) for more details.
+ ///
+ /// ```example
+ /// >>> #set page(width: 140pt)
+ /// $ cancel(
+ /// sum x,
+ /// stroke: #(
+ /// paint: red,
+ /// thickness: 1.5pt,
+ /// dash: "dashed",
+ /// ),
+ /// ) $
+ /// ```
+ #[resolve]
+ #[fold]
+ #[default(PartialStroke {
+ // Default stroke has 0.5pt for better visuals.
+ thickness: Smart::Custom(Abs::pt(0.5)),
+ ..Default::default()
+ })]
+ pub stroke: PartialStroke,
+}
+
+impl LayoutMath for CancelElem {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let body = ctx.layout_fragment(&self.body())?;
+ // Use the same math class as the body, in order to preserve automatic spacing around it.
+ let body_class = body.class().unwrap_or(MathClass::Special);
+ let mut body = body.into_frame();
+
+ let styles = ctx.styles();
+ let body_size = body.size();
+ let span = self.span();
+ let length = self.length(styles).resolve(styles);
+
+ let stroke = self.stroke(styles).unwrap_or(Stroke {
+ paint: TextElem::fill_in(styles),
+ ..Default::default()
+ });
+
+ let invert = self.inverted(styles);
+ let cross = self.cross(styles);
+ let angle = self.rotation(styles);
+
+ let invert_first_line = !cross && invert;
+ let first_line = draw_cancel_line(
+ length,
+ stroke.clone(),
+ invert_first_line,
+ angle,
+ body_size,
+ span,
+ );
+
+ // The origin of our line is the very middle of the element.
+ let center = body_size.to_point() / 2.0;
+ body.push_frame(center, first_line);
+
+ if cross {
+ // Draw the second line.
+ let second_line =
+ draw_cancel_line(length, stroke, true, angle, body_size, span);
+
+ body.push_frame(center, second_line);
+ }
+
+ ctx.push(FrameFragment::new(ctx, body).with_class(body_class));
+
+ Ok(())
+ }
+}
+
+/// Draws a cancel line.
+fn draw_cancel_line(
+ length: Rel<Abs>,
+ stroke: Stroke,
+ invert: bool,
+ angle: Angle,
+ body_size: Size,
+ span: Span,
+) -> Frame {
+ // B
+ // /|
+ // diagonal / | height
+ // / |
+ // / |
+ // O ----
+ // width
+ let diagonal = body_size.to_point().hypot();
+ let length = length.relative_to(diagonal);
+ let (width, height) = (body_size.x, body_size.y);
+ let mid = body_size / 2.0;
+
+ // Scale the amount needed such that the cancel line has the given 'length'
+ // (reference length, or 100%, is the whole diagonal).
+ // Scales from the center.
+ let scale = length.to_raw() / diagonal.to_raw();
+
+ // invert horizontally if 'invert' was given
+ let scale_x = scale * if invert { -1.0 } else { 1.0 };
+ let scale_y = scale;
+ let scales = Axes::new(scale_x, scale_y);
+
+ // Draw a line from bottom left to top right of the given element, where the
+ // origin represents the very middle of that element, that is, a line from
+ // (-width / 2, height / 2) with length components (width, -height) (sign is
+ // inverted in the y-axis). After applying the scale, the line will have the
+ // correct length and orientation (inverted if needed).
+ let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s);
+ let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s);
+
+ let mut frame = Frame::new(body_size);
+ frame.push(
+ start.to_point(),
+ FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span),
+ );
+
+ // Having the middle of the line at the origin is convenient here.
+ frame.transform(Transform::rotate(angle));
+ frame
+}
diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs
new file mode 100644
index 00000000..a1dc6cf4
--- /dev/null
+++ b/crates/typst-library/src/math/ctx.rs
@@ -0,0 +1,268 @@
+use ttf_parser::math::MathValue;
+use typst::font::{FontStyle, FontWeight};
+use typst::model::realize;
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::*;
+
+macro_rules! scaled {
+ ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => {
+ match $ctx.style.size {
+ MathSize::Display => scaled!($ctx, $display),
+ _ => scaled!($ctx, $text),
+ }
+ };
+ ($ctx:expr, $name:ident) => {
+ $ctx.constants.$name().scaled($ctx)
+ };
+}
+
+macro_rules! percent {
+ ($ctx:expr, $name:ident) => {
+ $ctx.constants.$name() as f64 / 100.0
+ };
+}
+
+/// The context for math layout.
+pub struct MathContext<'a, 'b, 'v> {
+ pub vt: &'v mut Vt<'b>,
+ pub regions: Regions<'static>,
+ pub font: &'a Font,
+ pub ttf: &'a ttf_parser::Face<'a>,
+ pub table: ttf_parser::math::Table<'a>,
+ pub constants: ttf_parser::math::Constants<'a>,
+ pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>,
+ pub space_width: Em,
+ pub fragments: Vec<MathFragment>,
+ pub local: Styles,
+ pub style: MathStyle,
+ pub size: Abs,
+ outer: StyleChain<'a>,
+ style_stack: Vec<(MathStyle, Abs)>,
+}
+
+impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
+ pub fn new(
+ vt: &'v mut Vt<'b>,
+ styles: StyleChain<'a>,
+ regions: Regions,
+ font: &'a Font,
+ block: bool,
+ ) -> Self {
+ let table = font.ttf().tables().math.unwrap();
+ let constants = table.constants.unwrap();
+
+ let ssty_table = font
+ .ttf()
+ .tables()
+ .gsub
+ .and_then(|gsub| {
+ gsub.features
+ .find(ttf_parser::Tag::from_bytes(b"ssty"))
+ .and_then(|feature| feature.lookup_indices.get(0))
+ .and_then(|index| gsub.lookups.get(index))
+ })
+ .and_then(|ssty| {
+ ssty.subtables.get::<ttf_parser::gsub::SubstitutionSubtable>(0)
+ })
+ .and_then(|ssty| match ssty {
+ ttf_parser::gsub::SubstitutionSubtable::Alternate(alt_glyphs) => {
+ Some(alt_glyphs)
+ }
+ _ => None,
+ });
+
+ let size = TextElem::size_in(styles);
+ let ttf = font.ttf();
+ let space_width = ttf
+ .glyph_index(' ')
+ .and_then(|id| ttf.glyph_hor_advance(id))
+ .map(|advance| font.to_em(advance))
+ .unwrap_or(THICK);
+
+ let variant = variant(styles);
+ Self {
+ vt,
+ regions: Regions::one(regions.base(), Axes::splat(false)),
+ font,
+ ttf: font.ttf(),
+ table,
+ constants,
+ ssty_table,
+ space_width,
+ fragments: vec![],
+ local: Styles::new(),
+ style: MathStyle {
+ variant: MathVariant::Serif,
+ size: if block { MathSize::Display } else { MathSize::Text },
+ cramped: false,
+ bold: variant.weight >= FontWeight::BOLD,
+ italic: match variant.style {
+ FontStyle::Normal => Smart::Auto,
+ FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true),
+ },
+ },
+ size,
+ outer: styles,
+ style_stack: vec![],
+ }
+ }
+
+ pub fn push(&mut self, fragment: impl Into<MathFragment>) {
+ self.fragments.push(fragment.into());
+ }
+
+ pub fn extend(&mut self, fragments: Vec<MathFragment>) {
+ self.fragments.extend(fragments);
+ }
+
+ pub fn layout_fragment(
+ &mut self,
+ elem: &dyn LayoutMath,
+ ) -> SourceResult<MathFragment> {
+ let row = self.layout_fragments(elem)?;
+ Ok(MathRow::new(row).into_fragment(self))
+ }
+
+ pub fn layout_fragments(
+ &mut self,
+ elem: &dyn LayoutMath,
+ ) -> SourceResult<Vec<MathFragment>> {
+ let prev = std::mem::take(&mut self.fragments);
+ elem.layout_math(self)?;
+ Ok(std::mem::replace(&mut self.fragments, prev))
+ }
+
+ pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> {
+ let fragments = self.layout_fragments(elem)?;
+ Ok(MathRow::new(fragments))
+ }
+
+ pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult<Frame> {
+ Ok(self.layout_fragment(elem)?.into_frame())
+ }
+
+ pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> {
+ Ok(content
+ .layout(self.vt, self.outer.chain(&self.local), self.regions)?
+ .into_frame())
+ }
+
+ pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult<MathFragment> {
+ let text = elem.text();
+ let span = elem.span();
+ let mut chars = text.chars();
+ let fragment = if let Some(mut glyph) = chars
+ .next()
+ .filter(|_| chars.next().is_none())
+ .map(|c| self.style.styled_char(c))
+ .and_then(|c| GlyphFragment::try_new(self, c, span))
+ {
+ // A single letter that is available in the math font.
+ match self.style.size {
+ MathSize::Display => {
+ if glyph.class == Some(MathClass::Large) {
+ let height = scaled!(self, display_operator_min_height);
+ glyph.stretch_vertical(self, height, Abs::zero()).into()
+ } else {
+ glyph.into()
+ }
+ }
+ MathSize::Script => {
+ glyph.make_scriptsize(self);
+ glyph.into()
+ }
+ MathSize::ScriptScript => {
+ glyph.make_scriptscriptsize(self);
+ glyph.into()
+ }
+ _ => glyph.into(),
+ }
+ } else if text.chars().all(|c| c.is_ascii_digit()) {
+ // Numbers aren't that difficult.
+ let mut fragments = vec![];
+ for c in text.chars() {
+ let c = self.style.styled_char(c);
+ fragments.push(GlyphFragment::new(self, c, span).into());
+ }
+ let frame = MathRow::new(fragments).into_frame(self);
+ FrameFragment::new(self, frame).into()
+ } else {
+ // Anything else is handled by Typst's standard text layout.
+ let spaced = text.graphemes(true).nth(1).is_some();
+ let mut style = self.style;
+ if self.style.italic == Smart::Auto {
+ style = style.with_italic(false);
+ }
+ let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect();
+ let frame = self.layout_content(&TextElem::packed(text).spanned(span))?;
+ FrameFragment::new(self, frame)
+ .with_class(MathClass::Alphabetic)
+ .with_spaced(spaced)
+ .into()
+ };
+ Ok(fragment)
+ }
+
+ pub fn styles(&self) -> StyleChain {
+ self.outer.chain(&self.local)
+ }
+
+ pub fn realize(&mut self, content: &Content) -> SourceResult<Option<Content>> {
+ realize(self.vt, content, self.outer.chain(&self.local))
+ }
+
+ pub fn style(&mut self, style: MathStyle) {
+ self.style_stack.push((self.style, self.size));
+ let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self);
+ self.size = base_size * style.size.factor(self);
+ self.local.set(TextElem::set_size(TextSize(self.size.into())));
+ self.local
+ .set(TextElem::set_style(if style.italic == Smart::Custom(true) {
+ FontStyle::Italic
+ } else {
+ FontStyle::Normal
+ }));
+ self.local.set(TextElem::set_weight(if style.bold {
+ FontWeight::BOLD
+ } else {
+ FontWeight::REGULAR
+ }));
+ self.style = style;
+ }
+
+ pub fn unstyle(&mut self) {
+ (self.style, self.size) = self.style_stack.pop().unwrap();
+ self.local.unset();
+ self.local.unset();
+ self.local.unset();
+ }
+}
+
+pub(super) trait Scaled {
+ fn scaled(self, ctx: &MathContext) -> Abs;
+}
+
+impl Scaled for i16 {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ ctx.font.to_em(self).scaled(ctx)
+ }
+}
+
+impl Scaled for u16 {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ ctx.font.to_em(self).scaled(ctx)
+ }
+}
+
+impl Scaled for Em {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ self.at(ctx.size)
+ }
+}
+
+impl Scaled for MathValue<'_> {
+ fn scaled(self, ctx: &MathContext) -> Abs {
+ self.value.scaled(ctx)
+ }
+}
diff --git a/crates/typst-library/src/math/delimited.rs b/crates/typst-library/src/math/delimited.rs
new file mode 100644
index 00000000..99cd6c33
--- /dev/null
+++ b/crates/typst-library/src/math/delimited.rs
@@ -0,0 +1,200 @@
+use super::*;
+
+/// How much less high scaled delimiters can be than what they wrap.
+pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
+
+/// Scales delimiters.
+///
+/// While matched delimiters scale by default, this can be used to scale
+/// unmatched delimiters and to control the delimiter scaling more precisely.
+///
+/// ## Example { #example }
+/// ```example
+/// $ lr(]a, b/2]) $
+/// $ lr(]sum_(x=1)^n] x, size: #50%) $
+/// ```
+///
+/// Display: Left/Right
+/// Category: math
+#[element(LayoutMath)]
+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 body = Content::empty();
+ for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
+ if i > 0 {
+ body += TextElem::packed(',');
+ }
+ body += arg;
+ }
+ body
+ )]
+ pub body: Content,
+}
+
+impl LayoutMath for LrElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut body = self.body();
+ if let Some(elem) = body.to::<LrElem>() {
+ if elem.size(ctx.styles()).is_auto() {
+ body = elem.body();
+ }
+ }
+
+ let mut fragments = ctx.layout_fragments(&body)?;
+ let axis = scaled!(ctx, axis_height);
+ let max_extent = fragments
+ .iter()
+ .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
+ .max()
+ .unwrap_or_default();
+
+ let height = self
+ .size(ctx.styles())
+ .unwrap_or(Rel::one())
+ .resolve(ctx.styles())
+ .relative_to(2.0 * max_extent);
+
+ match fragments.as_mut_slice() {
+ [one] => scale(ctx, one, height, None),
+ [first, .., last] => {
+ scale(ctx, first, height, Some(MathClass::Opening));
+ scale(ctx, last, height, Some(MathClass::Closing));
+ }
+ _ => {}
+ }
+
+ ctx.extend(fragments);
+
+ Ok(())
+ }
+}
+
+/// Scale a math fragment to a height.
+fn scale(
+ ctx: &mut MathContext,
+ fragment: &mut MathFragment,
+ height: Abs,
+ apply: Option<MathClass>,
+) {
+ if matches!(
+ fragment.class(),
+ Some(MathClass::Opening | MathClass::Closing | MathClass::Fence)
+ ) {
+ let glyph = match fragment {
+ MathFragment::Glyph(glyph) => glyph.clone(),
+ MathFragment::Variant(variant) => {
+ GlyphFragment::new(ctx, variant.c, variant.span)
+ }
+ _ => return,
+ };
+
+ let short_fall = DELIM_SHORT_FALL.scaled(ctx);
+ *fragment =
+ MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall));
+
+ if let Some(class) = apply {
+ fragment.set_class(class);
+ }
+ }
+}
+
+/// Floors an expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ floor(x/2) $
+/// ```
+///
+/// Display: Floor
+/// Category: math
+#[func]
+pub fn floor(
+ /// The expression to floor.
+ body: Content,
+) -> Content {
+ delimited(body, '⌊', '⌋')
+}
+
+/// Ceils an expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ ceil(x/2) $
+/// ```
+///
+/// Display: Ceil
+/// Category: math
+#[func]
+pub fn ceil(
+ /// The expression to ceil.
+ body: Content,
+) -> Content {
+ delimited(body, '⌈', '⌉')
+}
+
+/// Rounds an expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ round(x/2) $
+/// ```
+///
+/// Display: Round
+/// Category: math
+#[func]
+pub fn round(
+ /// The expression to round.
+ body: Content,
+) -> Content {
+ delimited(body, '⌊', '⌉')
+}
+
+/// Takes the absolute value of an expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ abs(x/2) $
+/// ```
+///
+///
+/// Display: Abs
+/// Category: math
+#[func]
+pub fn abs(
+ /// The expression to take the absolute value of.
+ body: Content,
+) -> Content {
+ delimited(body, '|', '|')
+}
+
+/// Takes the norm of an expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ norm(x/2) $
+/// ```
+///
+/// Display: Norm
+/// Category: math
+#[func]
+pub fn norm(
+ /// The expression to take the norm of.
+ body: Content,
+) -> Content {
+ delimited(body, '‖', '‖')
+}
+
+fn delimited(body: Content, left: char, right: char) -> Content {
+ LrElem::new(Content::sequence([
+ TextElem::packed(left),
+ body,
+ TextElem::packed(right),
+ ]))
+ .pack()
+}
diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs
new file mode 100644
index 00000000..0e1f78cc
--- /dev/null
+++ b/crates/typst-library/src/math/frac.rs
@@ -0,0 +1,148 @@
+use super::*;
+
+const FRAC_AROUND: Em = Em::new(0.1);
+
+/// A mathematical fraction.
+///
+/// ## Example { #example }
+/// ```example
+/// $ 1/2 < (x+1)/2 $
+/// $ ((x+1)) / 2 = frac(a, b) $
+/// ```
+///
+/// ## Syntax { #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.
+///
+/// Display: Fraction
+/// Category: math
+#[element(LayoutMath)]
+pub struct FracElem {
+ /// The fraction's numerator.
+ #[required]
+ pub num: Content,
+
+ /// The fraction's denominator.
+ #[required]
+ pub denom: Content,
+}
+
+impl LayoutMath for FracElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout(ctx, &self.num(), &self.denom(), false, self.span())
+ }
+}
+
+/// A binomial expression.
+///
+/// ## Example { #example }
+/// ```example
+/// $ binom(n, k) $
+/// ```
+///
+/// Display: Binomial
+/// Category: math
+#[element(LayoutMath)]
+pub struct BinomElem {
+ /// The binomial's upper index.
+ #[required]
+ pub upper: Content,
+
+ /// The binomial's lower index.
+ #[required]
+ pub lower: Content,
+}
+
+impl LayoutMath for BinomElem {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout(ctx, &self.upper(), &self.lower(), true, self.span())
+ }
+}
+
+/// Layout a fraction or binomial.
+fn layout(
+ ctx: &mut MathContext,
+ num: &Content,
+ denom: &Content,
+ binom: bool,
+ span: Span,
+) -> SourceResult<()> {
+ let short_fall = DELIM_SHORT_FALL.scaled(ctx);
+ let axis = scaled!(ctx, axis_height);
+ let thickness = scaled!(ctx, fraction_rule_thickness);
+ let shift_up = scaled!(
+ ctx,
+ text: fraction_numerator_shift_up,
+ display: fraction_numerator_display_style_shift_up,
+ );
+ let shift_down = scaled!(
+ ctx,
+ text: fraction_denominator_shift_down,
+ display: fraction_denominator_display_style_shift_down,
+ );
+ let num_min = scaled!(
+ ctx,
+ text: fraction_numerator_gap_min,
+ display: fraction_num_display_style_gap_min,
+ );
+ let denom_min = scaled!(
+ ctx,
+ text: fraction_denominator_gap_min,
+ display: fraction_denom_display_style_gap_min,
+ );
+
+ ctx.style(ctx.style.for_numerator());
+ let num = ctx.layout_frame(num)?;
+ ctx.unstyle();
+
+ ctx.style(ctx.style.for_denominator());
+ let denom = ctx.layout_frame(denom)?;
+ ctx.unstyle();
+
+ let around = FRAC_AROUND.scaled(ctx);
+ let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0);
+ let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0);
+
+ let line_width = num.width().max(denom.width());
+ let width = line_width + 2.0 * around;
+ let height = num.height() + num_gap + thickness + denom_gap + denom.height();
+ let size = Size::new(width, height);
+ let num_pos = Point::with_x((width - num.width()) / 2.0);
+ let line_pos =
+ Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0);
+ let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height());
+ let baseline = line_pos.y + axis;
+
+ let mut frame = Frame::new(size);
+ frame.set_baseline(baseline);
+ frame.push_frame(num_pos, num);
+ frame.push_frame(denom_pos, denom);
+
+ if binom {
+ ctx.push(
+ GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall),
+ );
+ ctx.push(FrameFragment::new(ctx, frame));
+ ctx.push(
+ GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall),
+ );
+ } else {
+ frame.push(
+ line_pos,
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(line_width)).stroked(Stroke {
+ paint: TextElem::fill_in(ctx.styles()),
+ thickness,
+ ..Stroke::default()
+ }),
+ span,
+ ),
+ );
+ ctx.push(FrameFragment::new(ctx, frame));
+ }
+
+ Ok(())
+}
diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs
new file mode 100644
index 00000000..139ce07b
--- /dev/null
+++ b/crates/typst-library/src/math/fragment.rs
@@ -0,0 +1,414 @@
+use super::*;
+use ttf_parser::gsub::AlternateSet;
+
+#[derive(Debug, Clone)]
+pub enum MathFragment {
+ Glyph(GlyphFragment),
+ Variant(VariantFragment),
+ Frame(FrameFragment),
+ Spacing(Abs),
+ Space(Abs),
+ Linebreak,
+ Align,
+}
+
+impl MathFragment {
+ pub fn size(&self) -> Size {
+ Size::new(self.width(), self.height())
+ }
+
+ pub fn width(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.width,
+ Self::Variant(variant) => variant.frame.width(),
+ Self::Frame(fragment) => fragment.frame.width(),
+ Self::Spacing(amount) => *amount,
+ Self::Space(amount) => *amount,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn height(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.height(),
+ Self::Variant(variant) => variant.frame.height(),
+ Self::Frame(fragment) => fragment.frame.height(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn ascent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.ascent,
+ Self::Variant(variant) => variant.frame.ascent(),
+ Self::Frame(fragment) => fragment.frame.baseline(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn descent(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.descent,
+ Self::Variant(variant) => variant.frame.descent(),
+ Self::Frame(fragment) => fragment.frame.descent(),
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn class(&self) -> Option<MathClass> {
+ match self {
+ Self::Glyph(glyph) => glyph.class,
+ Self::Variant(variant) => variant.class,
+ Self::Frame(fragment) => Some(fragment.class),
+ _ => None,
+ }
+ }
+
+ pub fn style(&self) -> Option<MathStyle> {
+ match self {
+ Self::Glyph(glyph) => Some(glyph.style),
+ Self::Variant(variant) => Some(variant.style),
+ Self::Frame(fragment) => Some(fragment.style),
+ _ => None,
+ }
+ }
+
+ pub fn font_size(&self) -> Option<Abs> {
+ match self {
+ Self::Glyph(glyph) => Some(glyph.font_size),
+ Self::Variant(variant) => Some(variant.font_size),
+ Self::Frame(fragment) => Some(fragment.font_size),
+ _ => None,
+ }
+ }
+
+ pub fn set_class(&mut self, class: MathClass) {
+ match self {
+ Self::Glyph(glyph) => glyph.class = Some(class),
+ Self::Variant(variant) => variant.class = Some(class),
+ Self::Frame(fragment) => fragment.class = class,
+ _ => {}
+ }
+ }
+
+ pub fn set_limits(&mut self, limits: Limits) {
+ match self {
+ Self::Glyph(glyph) => glyph.limits = limits,
+ Self::Variant(variant) => variant.limits = limits,
+ Self::Frame(fragment) => fragment.limits = limits,
+ _ => {}
+ }
+ }
+
+ pub fn is_spaced(&self) -> bool {
+ match self {
+ MathFragment::Frame(frame) => frame.spaced,
+ _ => self.class() == Some(MathClass::Fence),
+ }
+ }
+
+ pub fn italics_correction(&self) -> Abs {
+ match self {
+ Self::Glyph(glyph) => glyph.italics_correction,
+ Self::Variant(variant) => variant.italics_correction,
+ _ => Abs::zero(),
+ }
+ }
+
+ pub fn into_frame(self) -> Frame {
+ match self {
+ Self::Glyph(glyph) => glyph.into_frame(),
+ Self::Variant(variant) => variant.frame,
+ Self::Frame(fragment) => fragment.frame,
+ _ => Frame::new(self.size()),
+ }
+ }
+
+ pub fn limits(&self) -> Limits {
+ match self {
+ MathFragment::Glyph(glyph) => glyph.limits,
+ MathFragment::Variant(variant) => variant.limits,
+ MathFragment::Frame(fragment) => fragment.limits,
+ _ => Limits::Never,
+ }
+ }
+}
+
+impl From<GlyphFragment> for MathFragment {
+ fn from(glyph: GlyphFragment) -> Self {
+ Self::Glyph(glyph)
+ }
+}
+
+impl From<VariantFragment> for MathFragment {
+ fn from(variant: VariantFragment) -> Self {
+ Self::Variant(variant)
+ }
+}
+
+impl From<FrameFragment> for MathFragment {
+ fn from(fragment: FrameFragment) -> Self {
+ Self::Frame(fragment)
+ }
+}
+
+#[derive(Clone)]
+pub struct GlyphFragment {
+ pub id: GlyphId,
+ pub c: char,
+ pub font: Font,
+ pub lang: Lang,
+ pub fill: Paint,
+ pub width: Abs,
+ pub ascent: Abs,
+ pub descent: Abs,
+ pub italics_correction: Abs,
+ pub style: MathStyle,
+ pub font_size: Abs,
+ pub class: Option<MathClass>,
+ pub span: Span,
+ pub meta: Vec<Meta>,
+ pub limits: Limits,
+}
+
+impl GlyphFragment {
+ pub fn new(ctx: &MathContext, c: char, span: Span) -> Self {
+ let id = ctx.ttf.glyph_index(c).unwrap_or_default();
+ Self::with_id(ctx, c, id, span)
+ }
+
+ pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option<Self> {
+ let c = ctx.style.styled_char(c);
+ let id = ctx.ttf.glyph_index(c)?;
+ Some(Self::with_id(ctx, c, id, span))
+ }
+
+ pub fn with_id(ctx: &MathContext, c: char, id: GlyphId, span: Span) -> Self {
+ let class = match c {
+ ':' => Some(MathClass::Relation),
+ _ => unicode_math_class::class(c),
+ };
+ let mut fragment = Self {
+ id,
+ c,
+ font: ctx.font.clone(),
+ lang: TextElem::lang_in(ctx.styles()),
+ fill: TextElem::fill_in(ctx.styles()),
+ style: ctx.style,
+ font_size: ctx.size,
+ width: Abs::zero(),
+ ascent: Abs::zero(),
+ descent: Abs::zero(),
+ limits: Limits::for_char(c),
+ italics_correction: Abs::zero(),
+ class,
+ span,
+ meta: MetaElem::data_in(ctx.styles()),
+ };
+ fragment.set_id(ctx, id);
+ fragment
+ }
+
+ /// Sets element id and boxes in appropriate way without changing other
+ /// styles. This is used to replace the glyph with a stretch variant.
+ pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) {
+ let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
+ let italics = italics_correction(ctx, id).unwrap_or_default();
+ let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect {
+ x_min: 0,
+ y_min: 0,
+ x_max: 0,
+ y_max: 0,
+ });
+
+ let mut width = advance.scaled(ctx);
+ if !is_extended_shape(ctx, id) {
+ width += italics;
+ }
+
+ self.id = id;
+ self.width = width;
+ self.ascent = bbox.y_max.scaled(ctx);
+ self.descent = -bbox.y_min.scaled(ctx);
+ self.italics_correction = italics;
+ }
+
+ pub fn height(&self) -> Abs {
+ self.ascent + self.descent
+ }
+
+ pub fn into_variant(self) -> VariantFragment {
+ VariantFragment {
+ c: self.c,
+ id: Some(self.id),
+ style: self.style,
+ font_size: self.font_size,
+ italics_correction: self.italics_correction,
+ class: self.class,
+ span: self.span,
+ limits: self.limits,
+ frame: self.into_frame(),
+ }
+ }
+
+ pub fn into_frame(self) -> Frame {
+ let item = TextItem {
+ font: self.font.clone(),
+ size: self.font_size,
+ fill: self.fill,
+ lang: self.lang,
+ text: self.c.into(),
+ glyphs: vec![Glyph {
+ id: self.id.0,
+ x_advance: Em::from_length(self.width, self.font_size),
+ x_offset: Em::zero(),
+ range: 0..self.c.len_utf8() as u16,
+ span: (self.span, 0),
+ }],
+ };
+ let size = Size::new(self.width, self.ascent + self.descent);
+ let mut frame = Frame::new(size);
+ frame.set_baseline(self.ascent);
+ frame.push(Point::with_y(self.ascent), FrameItem::Text(item));
+ frame.meta_iter(self.meta);
+ frame
+ }
+
+ pub fn make_scriptsize(&mut self, ctx: &MathContext) {
+ let alt_id =
+ script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0));
+
+ if let Some(alt_id) = alt_id {
+ self.set_id(ctx, alt_id);
+ }
+ }
+
+ pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) {
+ let alts = script_alternatives(ctx, self.id);
+ let alt_id = alts
+ .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0)));
+
+ if let Some(alt_id) = alt_id {
+ self.set_id(ctx, alt_id);
+ }
+ }
+}
+
+impl Debug for GlyphFragment {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "GlyphFragment({:?})", self.c)
+ }
+}
+
+#[derive(Clone)]
+pub struct VariantFragment {
+ pub c: char,
+ pub id: Option<GlyphId>,
+ pub italics_correction: Abs,
+ pub frame: Frame,
+ pub style: MathStyle,
+ pub font_size: Abs,
+ pub class: Option<MathClass>,
+ pub span: Span,
+ pub limits: Limits,
+}
+
+impl Debug for VariantFragment {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "VariantFragment({:?})", self.c)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct FrameFragment {
+ pub frame: Frame,
+ pub style: MathStyle,
+ pub font_size: Abs,
+ pub class: MathClass,
+ pub limits: Limits,
+ pub spaced: bool,
+ pub base_ascent: Abs,
+}
+
+impl FrameFragment {
+ pub fn new(ctx: &MathContext, mut frame: Frame) -> Self {
+ let base_ascent = frame.ascent();
+ frame.meta(ctx.styles(), false);
+ Self {
+ frame,
+ font_size: ctx.size,
+ style: ctx.style,
+ class: MathClass::Normal,
+ limits: Limits::Never,
+ spaced: false,
+ base_ascent,
+ }
+ }
+
+ pub fn with_class(self, class: MathClass) -> Self {
+ Self { class, ..self }
+ }
+
+ pub fn with_limits(self, limits: Limits) -> Self {
+ Self { limits, ..self }
+ }
+
+ pub fn with_spaced(self, spaced: bool) -> Self {
+ Self { spaced, ..self }
+ }
+
+ pub fn with_base_ascent(self, base_ascent: Abs) -> Self {
+ Self { base_ascent, ..self }
+ }
+}
+
+/// Look up the italics correction for a glyph.
+fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> {
+ Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx))
+}
+
+/// Look up the script/scriptscript alternates for a glyph
+fn script_alternatives<'a>(
+ ctx: &MathContext<'a, '_, '_>,
+ id: GlyphId,
+) -> Option<AlternateSet<'a>> {
+ ctx.ssty_table.and_then(|ssty| {
+ ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index))
+ })
+}
+
+/// Look up the italics correction for a glyph.
+fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
+ ctx.table
+ .glyph_info
+ .and_then(|info| info.extended_shapes)
+ .and_then(|info| info.get(id))
+ .is_some()
+}
+
+/// Look up a kerning value at a specific corner and height.
+///
+/// This can be integrated once we've found a font that actually provides this
+/// data.
+#[allow(unused)]
+fn kern_at_height(
+ ctx: &MathContext,
+ id: GlyphId,
+ corner: Corner,
+ height: Abs,
+) -> Option<Abs> {
+ let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?;
+ let kern = match corner {
+ Corner::TopLeft => kerns.top_left,
+ Corner::TopRight => kerns.top_right,
+ Corner::BottomRight => kerns.bottom_right,
+ Corner::BottomLeft => kerns.bottom_left,
+ }?;
+
+ let mut i = 0;
+ while i < kern.count() && height > kern.height(i)?.scaled(ctx) {
+ i += 1;
+ }
+
+ Some(kern.kern(i)?.scaled(ctx))
+}
diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs
new file mode 100644
index 00000000..aaccc332
--- /dev/null
+++ b/crates/typst-library/src/math/matrix.rs
@@ -0,0 +1,313 @@
+use super::*;
+
+const ROW_GAP: Em = Em::new(0.5);
+const COL_GAP: Em = Em::new(0.5);
+const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
+
+/// A column vector.
+///
+/// Content in the vector's elements can be aligned with the `&` symbol.
+///
+/// ## Example { #example }
+/// ```example
+/// $ vec(a, b, c) dot vec(1, 2, 3)
+/// = a + 2b + 3c $
+/// ```
+///
+/// Display: Vector
+/// Category: math
+#[element(LayoutMath)]
+pub struct VecElem {
+ /// The delimiter to use.
+ ///
+ /// ```example
+ /// #set math.vec(delim: "[")
+ /// $ vec(1, 2) $
+ /// ```
+ #[default(Some(Delimiter::Paren))]
+ pub delim: Option<Delimiter>,
+
+ /// The elements of the vector.
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+impl LayoutMath for VecElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let delim = self.delim(ctx.styles());
+ let frame = layout_vec_body(ctx, &self.children(), Align::Center)?;
+ layout_delimiters(
+ ctx,
+ frame,
+ delim.map(Delimiter::open),
+ delim.map(Delimiter::close),
+ self.span(),
+ )
+ }
+}
+
+/// 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 that are in the same row can be aligned with the `&` symbol.
+///
+/// ## Example { #example }
+/// ```example
+/// $ mat(
+/// 1, 2, ..., 10;
+/// 2, 2, ..., 10;
+/// dots.v, dots.v, dots.down, dots.v;
+/// 10, 10, ..., 10;
+/// ) $
+/// ```
+///
+/// Display: Matrix
+/// Category: math
+#[element(LayoutMath)]
+pub struct MatElem {
+ /// The delimiter to use.
+ ///
+ /// ```example
+ /// #set math.mat(delim: "[")
+ /// $ mat(1, 2; 3, 4) $
+ /// ```
+ #[default(Some(Delimiter::Paren))]
+ pub delim: Option<Delimiter>,
+
+ /// 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>>,
+}
+
+impl LayoutMath for MatElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let delim = self.delim(ctx.styles());
+ let frame = layout_mat_body(ctx, &self.rows())?;
+ layout_delimiters(
+ ctx,
+ frame,
+ delim.map(Delimiter::open),
+ delim.map(Delimiter::close),
+ self.span(),
+ )
+ }
+}
+
+/// A case distinction.
+///
+/// Content across different branches can be aligned with the `&` symbol.
+///
+/// ## Example { #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",
+/// ) $
+/// ```
+///
+/// Display: Cases
+/// Category: math
+#[element(LayoutMath)]
+pub struct CasesElem {
+ /// The delimiter to use.
+ ///
+ /// ```example
+ /// #set math.cases(delim: "[")
+ /// $ x = cases(1, 2) $
+ /// ```
+ #[default(Delimiter::Brace)]
+ pub delim: Delimiter,
+
+ /// The branches of the case distinction.
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+impl LayoutMath for CasesElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let delim = self.delim(ctx.styles());
+ let frame = layout_vec_body(ctx, &self.children(), Align::Left)?;
+ layout_delimiters(ctx, frame, Some(delim.open()), None, self.span())
+ }
+}
+
+/// A vector / matrix delimiter.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum Delimiter {
+ /// Delimit with parentheses.
+ #[string("(")]
+ Paren,
+ /// Delimit with brackets.
+ #[string("[")]
+ Bracket,
+ /// Delimit with curly braces.
+ #[string("{")]
+ Brace,
+ /// Delimit with vertical bars.
+ #[string("|")]
+ Bar,
+ /// Delimit with double vertical bars.
+ #[string("||")]
+ DoubleBar,
+}
+
+impl Delimiter {
+ /// The delimiter's opening character.
+ fn open(self) -> char {
+ match self {
+ Self::Paren => '(',
+ Self::Bracket => '[',
+ Self::Brace => '{',
+ Self::Bar => '|',
+ Self::DoubleBar => '‖',
+ }
+ }
+
+ /// The delimiter's closing character.
+ fn close(self) -> char {
+ match self {
+ Self::Paren => ')',
+ Self::Bracket => ']',
+ Self::Brace => '}',
+ Self::Bar => '|',
+ Self::DoubleBar => '‖',
+ }
+ }
+}
+
+/// Layout the inner contents of a vector.
+fn layout_vec_body(
+ ctx: &mut MathContext,
+ column: &[Content],
+ align: Align,
+) -> SourceResult<Frame> {
+ let gap = ROW_GAP.scaled(ctx);
+ ctx.style(ctx.style.for_denominator());
+ let mut flat = vec![];
+ for child in column {
+ flat.push(ctx.layout_row(child)?);
+ }
+ ctx.unstyle();
+ Ok(stack(ctx, flat, align, gap, 0))
+}
+
+/// Layout the inner contents of a matrix.
+fn layout_mat_body(ctx: &mut MathContext, rows: &[Vec<Content>]) -> SourceResult<Frame> {
+ let row_gap = ROW_GAP.scaled(ctx);
+ let col_gap = COL_GAP.scaled(ctx);
+
+ let ncols = rows.first().map_or(0, |row| row.len());
+ let nrows = rows.len();
+ if ncols == 0 || nrows == 0 {
+ return Ok(Frame::new(Size::zero()));
+ }
+
+ let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
+
+ ctx.style(ctx.style.for_denominator());
+ let mut cols = vec![vec![]; ncols];
+ for (row, (ascent, descent)) in rows.iter().zip(&mut heights) {
+ for (cell, col) in row.iter().zip(&mut cols) {
+ let cell = ctx.layout_row(cell)?;
+ ascent.set_max(cell.ascent());
+ descent.set_max(cell.descent());
+ col.push(cell);
+ }
+ }
+ ctx.unstyle();
+
+ let mut frame = Frame::new(Size::new(
+ Abs::zero(),
+ heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + row_gap * (nrows - 1) as f64,
+ ));
+ let mut x = Abs::zero();
+ for col in cols {
+ let AlignmentResult { points, width: rcol } = alignments(&col);
+ let mut y = Abs::zero();
+ for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
+ let cell = cell.into_aligned_frame(ctx, &points, Align::Center);
+ let pos = Point::new(
+ if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x },
+ y + ascent - cell.ascent(),
+ );
+ frame.push_frame(pos, cell);
+ y += ascent + descent + row_gap;
+ }
+ x += rcol + col_gap;
+ }
+ frame.size_mut().x = x - col_gap;
+
+ Ok(frame)
+}
+
+/// Layout the outer wrapper around a vector's or matrices' body.
+fn layout_delimiters(
+ ctx: &mut MathContext,
+ mut frame: Frame,
+ left: Option<char>,
+ right: Option<char>,
+ span: Span,
+) -> SourceResult<()> {
+ let axis = scaled!(ctx, axis_height);
+ let short_fall = DELIM_SHORT_FALL.scaled(ctx);
+ let height = frame.height();
+ let target = height + VERTICAL_PADDING.of(height);
+ frame.set_baseline(height / 2.0 + axis);
+
+ if let Some(left) = left {
+ ctx.push(
+ GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall),
+ );
+ }
+
+ ctx.push(FrameFragment::new(ctx, frame));
+
+ if let Some(right) = right {
+ ctx.push(
+ GlyphFragment::new(ctx, right, span)
+ .stretch_vertical(ctx, target, short_fall),
+ );
+ }
+
+ Ok(())
+}
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(())
+ }
+}
diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs
new file mode 100644
index 00000000..8ad74c49
--- /dev/null
+++ b/crates/typst-library/src/math/op.rs
@@ -0,0 +1,113 @@
+use typst::eval::Scope;
+
+use super::*;
+
+/// A text operator in an equation.
+///
+/// ## Example { #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`, `ctg`, `coth`, `csc`, `deg`, `det`, `dim`,
+/// `exp`, `gcd`, `hom`, `mod`, `inf`, `ker`, `lg`, `lim`, `ln`, `log`,
+/// `max`, `min`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`, `tan`, `tg`,
+/// `tanh`, `liminf`, and `limsup`.
+///
+/// Display: Text Operator
+/// Category: math
+#[element(LayoutMath)]
+pub struct OpElem {
+ /// The operator's text.
+ #[required]
+ pub text: EcoString,
+
+ /// Whether the operator should show attachments as limits in display mode.
+ #[default(false)]
+ pub limits: bool,
+}
+
+impl LayoutMath for OpElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let fragment =
+ ctx.layout_text(&TextElem::new(self.text()).spanned(self.span()))?;
+ ctx.push(
+ FrameFragment::new(ctx, fragment.into_frame())
+ .with_class(MathClass::Large)
+ .with_limits(if self.limits(ctx.styles()) {
+ Limits::Display
+ } else {
+ Limits::Never
+ }),
+ );
+ Ok(())
+ }
+}
+
+macro_rules! ops {
+ ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => {
+ pub(super) fn define(math: &mut Scope) {
+ $(math.define(
+ stringify!($name),
+ OpElem::new(ops!(@name $name $(: $value)?).into())
+ .with_limits(ops!(@limit $($tts)*))
+ .pack()
+ );)*
+
+ let dif = |d| {
+ HElem::new(THIN.into()).pack()
+ + MathStyleElem::new(TextElem::packed(d)).with_italic(Some(false)).pack()
+ };
+ 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,
+ ctg,
+ coth,
+ csc,
+ deg,
+ det (limits),
+ dim,
+ exp,
+ gcd (limits),
+ hom,
+ mod,
+ inf (limits),
+ ker,
+ lg,
+ lim (limits),
+ ln,
+ log,
+ max (limits),
+ min (limits),
+ Pr (limits),
+ sec,
+ sin,
+ sinc,
+ sinh,
+ sup (limits),
+ tan,
+ tg,
+ tanh,
+ liminf: "lim inf" (limits),
+ limsup: "lim sup" (limits),
+}
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
new file mode 100644
index 00000000..d1c5f46a
--- /dev/null
+++ b/crates/typst-library/src/math/root.rs
@@ -0,0 +1,156 @@
+use super::*;
+
+/// A square root.
+///
+/// ## Example { #example }
+/// ```example
+/// $ sqrt(x^2) = x = sqrt(x)^2 $
+/// ```
+///
+/// Display: Square Root
+/// Category: math
+#[func]
+pub fn sqrt(
+ /// The expression to take the square root of.
+ radicand: Content,
+) -> Content {
+ RootElem::new(radicand).pack()
+}
+
+/// A general root.
+///
+/// ## Example { #example }
+/// ```example
+/// $ root(3, x) $
+/// ```
+///
+/// Display: Root
+/// Category: math
+#[element(LayoutMath)]
+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,
+}
+
+impl LayoutMath for RootElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout(ctx, self.index(ctx.styles()).as_ref(), &self.radicand(), self.span())
+ }
+}
+
+/// Layout a root.
+///
+/// https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
+fn layout(
+ ctx: &mut MathContext,
+ mut index: Option<&Content>,
+ radicand: &Content,
+ span: Span,
+) -> SourceResult<()> {
+ let gap = scaled!(
+ ctx,
+ text: radical_vertical_gap,
+ display: radical_display_style_vertical_gap,
+ );
+ let thickness = scaled!(ctx, radical_rule_thickness);
+ let extra_ascender = scaled!(ctx, radical_extra_ascender);
+ let kern_before = scaled!(ctx, radical_kern_before_degree);
+ let kern_after = scaled!(ctx, radical_kern_after_degree);
+ let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
+
+ // Layout radicand.
+ ctx.style(ctx.style.with_cramped(true));
+ let radicand = ctx.layout_frame(radicand)?;
+ ctx.unstyle();
+
+ // Layout root symbol.
+ let target = radicand.height() + thickness + gap;
+ let sqrt = precomposed(ctx, index, target)
+ .map(|frame| {
+ index = None;
+ frame
+ })
+ .unwrap_or_else(|| {
+ let glyph = GlyphFragment::new(ctx, '√', span);
+ glyph.stretch_vertical(ctx, target, Abs::zero()).frame
+ });
+
+ // Layout the index.
+ // Script-script style looks too small, we use Script style instead.
+ ctx.style(ctx.style.with_size(MathSize::Script));
+ let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?;
+ ctx.unstyle();
+
+ let gap = gap.max((sqrt.height() - radicand.height() - thickness) / 2.0);
+ let descent = radicand.descent() + gap;
+ let inner_ascent = extra_ascender + thickness + gap + radicand.ascent();
+
+ let mut sqrt_offset = Abs::zero();
+ let mut shift_up = Abs::zero();
+ let mut ascent = inner_ascent;
+
+ if let Some(index) = &index {
+ sqrt_offset = kern_before + index.width() + kern_after;
+ shift_up = raise_factor * sqrt.height() - descent + index.descent();
+ ascent.set_max(shift_up + index.ascent());
+ }
+
+ let radicant_offset = sqrt_offset + sqrt.width();
+ let width = radicant_offset + radicand.width();
+ let size = Size::new(width, ascent + descent);
+
+ let sqrt_pos = Point::new(sqrt_offset, ascent - inner_ascent);
+ let line_pos = Point::new(radicant_offset, ascent - inner_ascent + thickness / 2.0);
+ let radicand_pos = Point::new(radicant_offset, ascent - radicand.ascent());
+
+ let mut frame = Frame::new(size);
+ frame.set_baseline(ascent);
+
+ if let Some(index) = index {
+ let index_pos = Point::new(kern_before, ascent - shift_up - index.ascent());
+ frame.push_frame(index_pos, index);
+ }
+
+ frame.push_frame(sqrt_pos, sqrt);
+ frame.push(
+ line_pos,
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke {
+ paint: TextElem::fill_in(ctx.styles()),
+ thickness,
+ ..Stroke::default()
+ }),
+ span,
+ ),
+ );
+
+ frame.push_frame(radicand_pos, radicand);
+ ctx.push(FrameFragment::new(ctx, frame));
+
+ Ok(())
+}
+
+/// Select a precomposed radical, if the font has it.
+fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> {
+ let elem = index?.to::<TextElem>()?;
+ let c = match elem.text().as_str() {
+ "3" => '∛',
+ "4" => '∜',
+ _ => return None,
+ };
+
+ ctx.ttf.glyph_index(c)?;
+ let glyph = GlyphFragment::new(ctx, c, elem.span());
+ let variant = glyph.stretch_vertical(ctx, target, Abs::zero()).frame;
+ if variant.height() < target {
+ return None;
+ }
+
+ Some(variant)
+}
diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs
new file mode 100644
index 00000000..687f82b8
--- /dev/null
+++ b/crates/typst-library/src/math/row.rs
@@ -0,0 +1,258 @@
+use std::iter::once;
+
+use crate::layout::AlignElem;
+
+use super::*;
+
+pub const TIGHT_LEADING: Em = Em::new(0.25);
+
+#[derive(Debug, Default, Clone)]
+pub struct MathRow(Vec<MathFragment>);
+
+impl MathRow {
+ pub fn new(fragments: Vec<MathFragment>) -> Self {
+ let iter = fragments.into_iter().peekable();
+ let mut last: Option<usize> = None;
+ let mut space: Option<MathFragment> = None;
+ let mut resolved: Vec<MathFragment> = vec![];
+
+ for mut fragment in iter {
+ match fragment {
+ // Keep space only if supported by spaced fragments.
+ MathFragment::Space(_) => {
+ if last.is_some() {
+ space = Some(fragment);
+ }
+ continue;
+ }
+
+ // Explicit spacing disables automatic spacing.
+ MathFragment::Spacing(_) => {
+ last = None;
+ space = None;
+ resolved.push(fragment);
+ continue;
+ }
+
+ // Alignment points are resolved later.
+ MathFragment::Align => {
+ resolved.push(fragment);
+ continue;
+ }
+
+ // New line, new things.
+ MathFragment::Linebreak => {
+ resolved.push(fragment);
+ space = None;
+ last = None;
+ continue;
+ }
+
+ _ => {}
+ }
+
+ // Convert variable operators into binary operators if something
+ // precedes them and they are not preceded by a operator or comparator.
+ if fragment.class() == Some(MathClass::Vary)
+ && matches!(
+ last.and_then(|i| resolved[i].class()),
+ Some(
+ MathClass::Normal
+ | MathClass::Alphabetic
+ | MathClass::Closing
+ | MathClass::Fence
+ )
+ )
+ {
+ fragment.set_class(MathClass::Binary);
+ }
+
+ // Insert spacing between the last and this item.
+ if let Some(i) = last {
+ if let Some(s) = spacing(&resolved[i], space.take(), &fragment) {
+ resolved.insert(i + 1, s);
+ }
+ }
+
+ last = Some(resolved.len());
+ resolved.push(fragment);
+ }
+
+ Self(resolved)
+ }
+
+ pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> {
+ self.0.iter()
+ }
+
+ /// Extract the sublines of the row.
+ ///
+ /// It is very unintuitive, but in current state of things, a `MathRow` can
+ /// contain several actual rows. That function deconstructs it to "single"
+ /// rows. Hopefully this is only a temporary hack.
+ pub fn rows(&self) -> Vec<Self> {
+ self.0
+ .split(|frag| matches!(frag, MathFragment::Linebreak))
+ .map(|slice| Self(slice.to_vec()))
+ .collect()
+ }
+
+ pub fn ascent(&self) -> Abs {
+ self.iter().map(MathFragment::ascent).max().unwrap_or_default()
+ }
+
+ pub fn descent(&self) -> Abs {
+ self.iter().map(MathFragment::descent).max().unwrap_or_default()
+ }
+
+ pub fn class(&self) -> MathClass {
+ // Predict the class of the output of 'into_fragment'
+ if self.0.len() == 1 {
+ self.0
+ .first()
+ .and_then(|fragment| fragment.class())
+ .unwrap_or(MathClass::Special)
+ } else {
+ // FrameFragment::new() (inside 'into_fragment' in this branch) defaults
+ // to MathClass::Normal for its class.
+ MathClass::Normal
+ }
+ }
+
+ pub fn into_frame(self, ctx: &MathContext) -> Frame {
+ let styles = ctx.styles();
+ let align = AlignElem::alignment_in(styles).x.resolve(styles);
+ self.into_aligned_frame(ctx, &[], align)
+ }
+
+ pub fn into_fragment(self, ctx: &MathContext) -> MathFragment {
+ if self.0.len() == 1 {
+ self.0.into_iter().next().unwrap()
+ } else {
+ FrameFragment::new(ctx, self.into_frame(ctx)).into()
+ }
+ }
+
+ pub fn into_aligned_frame(
+ self,
+ ctx: &MathContext,
+ points: &[Abs],
+ align: Align,
+ ) -> Frame {
+ if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
+ let leading = if ctx.style.size >= MathSize::Text {
+ ParElem::leading_in(ctx.styles())
+ } else {
+ TIGHT_LEADING.scaled(ctx)
+ };
+
+ let mut rows: Vec<_> = self.rows();
+
+ if matches!(rows.last(), Some(row) if row.0.is_empty()) {
+ rows.pop();
+ }
+
+ let AlignmentResult { points, width } = alignments(&rows);
+ let mut frame = Frame::new(Size::zero());
+
+ for (i, row) in rows.into_iter().enumerate() {
+ let sub = row.into_line_frame(&points, align);
+ let size = frame.size_mut();
+ if i > 0 {
+ size.y += leading;
+ }
+
+ let mut pos = Point::with_y(size.y);
+ if points.is_empty() {
+ pos.x = align.position(width - sub.width());
+ }
+ size.y += sub.height();
+ size.x.set_max(sub.width());
+ frame.push_frame(pos, sub);
+ }
+ frame
+ } else {
+ self.into_line_frame(points, align)
+ }
+ }
+
+ fn into_line_frame(self, points: &[Abs], align: Align) -> Frame {
+ let ascent = self.ascent();
+ let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent()));
+ frame.set_baseline(ascent);
+
+ let mut next_x = {
+ let mut widths = Vec::new();
+ if !points.is_empty() && align != Align::Left {
+ let mut width = Abs::zero();
+ for fragment in self.iter() {
+ if matches!(fragment, MathFragment::Align) {
+ widths.push(width);
+ width = Abs::zero();
+ } else {
+ width += fragment.width();
+ }
+ }
+ widths.push(width);
+ }
+ let widths = widths;
+
+ let mut prev_points = once(Abs::zero()).chain(points.iter().copied());
+ let mut point_widths = points.iter().copied().zip(widths);
+ let mut alternator = LeftRightAlternator::Right;
+ move || match align {
+ Align::Left => prev_points.next(),
+ Align::Right => point_widths.next().map(|(point, width)| point - width),
+ _ => point_widths
+ .next()
+ .zip(prev_points.next())
+ .zip(alternator.next())
+ .map(|(((point, width), prev_point), alternator)| match alternator {
+ LeftRightAlternator::Left => prev_point,
+ LeftRightAlternator::Right => point - width,
+ }),
+ }
+ };
+ let mut x = next_x().unwrap_or_default();
+
+ for fragment in self.0.into_iter() {
+ if matches!(fragment, MathFragment::Align) {
+ x = next_x().unwrap_or(x);
+ continue;
+ }
+
+ let y = ascent - fragment.ascent();
+ let pos = Point::new(x, y);
+ x += fragment.width();
+ frame.push_frame(pos, fragment.into_frame());
+ }
+
+ frame.size_mut().x = x;
+ frame
+ }
+}
+
+impl<T: Into<MathFragment>> From<T> for MathRow {
+ fn from(fragment: T) -> Self {
+ Self(vec![fragment.into()])
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+enum LeftRightAlternator {
+ Left,
+ Right,
+}
+
+impl Iterator for LeftRightAlternator {
+ type Item = LeftRightAlternator;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let r = Some(*self);
+ match self {
+ Self::Left => *self = Self::Right,
+ Self::Right => *self = Self::Left,
+ }
+ r
+ }
+}
diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs
new file mode 100644
index 00000000..848aca78
--- /dev/null
+++ b/crates/typst-library/src/math/spacing.rs
@@ -0,0 +1,60 @@
+use super::*;
+
+pub(super) const THIN: Em = Em::new(1.0 / 6.0);
+pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
+pub(super) const THICK: Em = Em::new(5.0 / 18.0);
+pub(super) const QUAD: Em = Em::new(1.0);
+
+/// Hook up all spacings.
+pub(super) fn define(math: &mut Scope) {
+ 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());
+}
+
+/// Create the spacing between two fragments in a given style.
+pub(super) fn spacing(
+ l: &MathFragment,
+ space: Option<MathFragment>,
+ r: &MathFragment,
+) -> Option<MathFragment> {
+ use MathClass::*;
+
+ let class = |f: &MathFragment| f.class().unwrap_or(Special);
+ let resolve = |v: Em, f: &MathFragment| {
+ Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size))))
+ };
+ let script =
+ |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script);
+
+ match (class(l), class(r)) {
+ // No spacing before punctuation; thin spacing after punctuation, unless
+ // in script size.
+ (_, Punctuation) => None,
+ (Punctuation, _) if !script(l) => resolve(THIN, l),
+
+ // No spacing after opening delimiters and before closing delimiters.
+ (Opening, _) | (_, Closing) => None,
+
+ // Thick spacing around relations, unless followed by a another relation
+ // or in script size.
+ (Relation, Relation) => None,
+ (Relation, _) if !script(l) => resolve(THICK, l),
+ (_, Relation) if !script(r) => resolve(THICK, r),
+
+ // Medium spacing around binary operators, unless in script size.
+ (Binary, _) if !script(l) => resolve(MEDIUM, l),
+ (_, Binary) if !script(r) => resolve(MEDIUM, r),
+
+ // Thin spacing around large operators, unless next to a delimiter.
+ (Large, Opening | Fence) | (Closing | Fence, Large) => None,
+ (Large, _) => resolve(THIN, l),
+ (_, Large) => resolve(THIN, r),
+
+ // Spacing around spaced frames.
+ _ if (l.is_spaced() || r.is_spaced()) => space,
+
+ _ => None,
+ }
+}
diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst-library/src/math/stretch.rs
new file mode 100644
index 00000000..910f7a81
--- /dev/null
+++ b/crates/typst-library/src/math/stretch.rs
@@ -0,0 +1,199 @@
+use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
+use ttf_parser::LazyArray16;
+
+use super::*;
+
+/// Maximum number of times extenders can be repeated.
+const MAX_REPEATS: usize = 1024;
+
+impl GlyphFragment {
+ /// Try to stretch a glyph to a desired height.
+ pub fn stretch_vertical(
+ self,
+ ctx: &MathContext,
+ height: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, height, short_fall, false)
+ }
+
+ /// Try to stretch a glyph to a desired width.
+ pub fn stretch_horizontal(
+ self,
+ ctx: &MathContext,
+ width: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, width, short_fall, true)
+ }
+}
+
+/// Try to stretch a glyph to a desired width or height.
+///
+/// The resulting frame may not have the exact desired width.
+fn stretch_glyph(
+ ctx: &MathContext,
+ mut base: GlyphFragment,
+ target: Abs,
+ short_fall: Abs,
+ horizontal: bool,
+) -> VariantFragment {
+ let short_target = target - short_fall;
+ let mut min_overlap = Abs::zero();
+ let construction = ctx
+ .table
+ .variants
+ .and_then(|variants| {
+ min_overlap = variants.min_connector_overlap.scaled(ctx);
+ if horizontal {
+ variants.horizontal_constructions
+ } else {
+ variants.vertical_constructions
+ }
+ .get(base.id)
+ })
+ .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
+
+ // If the base glyph is good enough, use it.
+ let advance = if horizontal { base.width } else { base.height() };
+ if short_target <= advance {
+ return base.into_variant();
+ }
+
+ // Search for a pre-made variant with a good advance.
+ let mut best_id = base.id;
+ let mut best_advance = base.width;
+ for variant in construction.variants {
+ best_id = variant.variant_glyph;
+ best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
+ if short_target <= best_advance {
+ break;
+ }
+ }
+
+ // This is either good or the best we've got.
+ if short_target <= best_advance || construction.assembly.is_none() {
+ base.set_id(ctx, best_id);
+ return base.into_variant();
+ }
+
+ // Assemble from parts.
+ let assembly = construction.assembly.unwrap();
+ assemble(ctx, base, assembly, min_overlap, target, horizontal)
+}
+
+/// Assemble a glyph from parts.
+fn assemble(
+ ctx: &MathContext,
+ base: GlyphFragment,
+ assembly: GlyphAssembly,
+ min_overlap: Abs,
+ target: Abs,
+ horizontal: bool,
+) -> VariantFragment {
+ // Determine the number of times the extenders need to be repeated as well
+ // as a ratio specifying how much to spread the parts apart
+ // (0 = maximal overlap, 1 = minimal overlap).
+ let mut full;
+ let mut ratio;
+ let mut repeat = 0;
+ loop {
+ full = Abs::zero();
+ ratio = 0.0;
+
+ let mut parts = parts(assembly, repeat).peekable();
+ let mut growable = Abs::zero();
+
+ while let Some(part) = parts.next() {
+ let mut advance = part.full_advance.scaled(ctx);
+ if let Some(next) = parts.peek() {
+ let max_overlap = part
+ .end_connector_length
+ .min(next.start_connector_length)
+ .scaled(ctx);
+
+ advance -= max_overlap;
+ growable += max_overlap - min_overlap;
+ }
+
+ full += advance;
+ }
+
+ if full < target {
+ let delta = target - full;
+ ratio = (delta / growable).min(1.0);
+ full += ratio * growable;
+ }
+
+ if target <= full || repeat >= MAX_REPEATS {
+ break;
+ }
+
+ repeat += 1;
+ }
+
+ let mut selected = vec![];
+ let mut parts = parts(assembly, repeat).peekable();
+ while let Some(part) = parts.next() {
+ let mut advance = part.full_advance.scaled(ctx);
+ if let Some(next) = parts.peek() {
+ let max_overlap =
+ part.end_connector_length.min(next.start_connector_length).scaled(ctx);
+ advance -= max_overlap;
+ advance += ratio * (max_overlap - min_overlap);
+ }
+
+ let mut fragment = base.clone();
+ fragment.set_id(ctx, part.glyph_id);
+ selected.push((fragment, advance));
+ }
+
+ let size;
+ let baseline;
+ if horizontal {
+ let height = base.ascent + base.descent;
+ size = Size::new(full, height);
+ baseline = base.ascent;
+ } else {
+ let axis = scaled!(ctx, axis_height);
+ let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
+ size = Size::new(width, full);
+ baseline = full / 2.0 + axis;
+ }
+
+ let mut frame = Frame::new(size);
+ let mut offset = Abs::zero();
+ frame.set_baseline(baseline);
+ frame.meta_iter(base.meta);
+
+ for (fragment, advance) in selected {
+ let pos = if horizontal {
+ Point::new(offset, frame.baseline() - fragment.ascent)
+ } else {
+ Point::with_y(full - offset - fragment.height())
+ };
+ frame.push_frame(pos, fragment.into_frame());
+ offset += advance;
+ }
+
+ VariantFragment {
+ c: base.c,
+ id: None,
+ frame,
+ style: base.style,
+ font_size: base.font_size,
+ italics_correction: Abs::zero(),
+ class: base.class,
+ span: base.span,
+ limits: base.limits,
+ }
+}
+
+/// Return an iterator over the assembly's parts with extenders repeated the
+/// specified number of times.
+fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
+ assembly.parts.into_iter().flat_map(move |part| {
+ let count = if part.part_flags.extender() { repeat } else { 1 };
+ std::iter::repeat(part).take(count)
+ })
+}
diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs
new file mode 100644
index 00000000..235770db
--- /dev/null
+++ b/crates/typst-library/src/math/style.rs
@@ -0,0 +1,620 @@
+use super::*;
+
+/// Bold font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// $ bold(A) := B^+ $
+/// ```
+///
+/// Display: Bold
+/// Category: math
+#[func]
+pub fn bold(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_bold(Some(true)).pack()
+}
+
+/// Upright (non-italic) font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// $ upright(A) != A $
+/// ```
+///
+/// Display: Upright
+/// Category: math
+#[func]
+pub fn upright(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_italic(Some(false)).pack()
+}
+
+/// Italic font style in math.
+///
+/// For roman letters and greek lowercase letters, this is already the default.
+///
+/// Display: Italic
+/// Category: math
+#[func]
+pub fn italic(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_italic(Some(true)).pack()
+}
+/// Serif (roman) font style in math.
+///
+/// This is already the default.
+///
+/// Display: Serif
+/// Category: math
+#[func]
+pub fn serif(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack()
+}
+
+/// Sans-serif font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// $ sans(A B C) $
+/// ```
+///
+/// Display: Sans-serif
+/// Category: math
+#[func]
+pub fn sans(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack()
+}
+
+/// Calligraphic font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// Let $cal(P)$ be the set of ...
+/// ```
+///
+/// Display: Calligraphic
+/// Category: math
+#[func]
+pub fn cal(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack()
+}
+
+/// Fraktur font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// $ frak(P) $
+/// ```
+///
+/// Display: Fraktur
+/// Category: math
+#[func]
+pub fn frak(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack()
+}
+
+/// Monospace font style in math.
+///
+/// ## Example { #example }
+/// ```example
+/// $ mono(x + y = z) $
+/// ```
+///
+/// Display: Monospace
+/// Category: math
+#[func]
+pub fn mono(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack()
+}
+
+/// 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 { #example }
+/// ```example
+/// $ bb(b) $
+/// $ bb(N) = NN $
+/// $ f: NN -> RR $
+/// ```
+///
+/// Display: Blackboard Bold
+/// Category: math
+#[func]
+pub fn bb(
+ /// The content to style.
+ body: Content,
+) -> Content {
+ MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack()
+}
+
+/// Forced display style in math.
+///
+/// This is the normal size for block equations.
+///
+/// ## Example { #example }
+/// ```example
+/// $sum_i x_i/2 = display(sum_i x_i/2)$
+/// ```
+///
+/// Display: Display Size
+/// Category: math
+#[func]
+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 {
+ MathStyleElem::new(body)
+ .with_size(Some(MathSize::Display))
+ .with_cramped(Some(cramped))
+ .pack()
+}
+
+/// Forced inline (text) style in math.
+///
+/// This is the normal size for inline equations.
+///
+/// ## Example { #example }
+/// ```example
+/// $ sum_i x_i/2
+/// = inline(sum_i x_i/2) $
+/// ```
+///
+/// Display: Inline Size
+/// Category: math
+#[func]
+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 {
+ MathStyleElem::new(body)
+ .with_size(Some(MathSize::Text))
+ .with_cramped(Some(cramped))
+ .pack()
+}
+
+/// Forced script style in math.
+///
+/// This is the smaller size used in powers or sub- or superscripts.
+///
+/// ## Example { #example }
+/// ```example
+/// $sum_i x_i/2 = script(sum_i x_i/2)$
+/// ```
+///
+/// Display: Script Size
+/// Category: math
+#[func]
+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 {
+ MathStyleElem::new(body)
+ .with_size(Some(MathSize::Script))
+ .with_cramped(Some(cramped))
+ .pack()
+}
+
+/// Forced second script style in math.
+///
+/// This is the smallest size, used in second-level sub- and superscripts
+/// (script of the script).
+///
+/// ## Example { #example }
+/// ```example
+/// $sum_i x_i/2 = sscript(sum_i x_i/2)$
+/// ```
+///
+/// Display: Script-Script Size
+/// Category: math
+#[func]
+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 {
+ MathStyleElem::new(body)
+ .with_size(Some(MathSize::ScriptScript))
+ .with_cramped(Some(cramped))
+ .pack()
+}
+
+/// A font variant in math.
+///
+/// Display: Bold
+/// Category: math
+#[element(LayoutMath)]
+pub struct MathStyleElem {
+ /// The content to style.
+ #[required]
+ pub body: Content,
+
+ /// The variant to select.
+ pub variant: Option<MathVariant>,
+
+ /// Whether to use bold glyphs.
+ pub bold: Option<bool>,
+
+ /// Whether to use italic glyphs.
+ pub italic: Option<bool>,
+
+ /// Whether to use forced size
+ pub size: Option<MathSize>,
+
+ /// Whether to limit height of exponents
+ pub cramped: Option<bool>,
+}
+
+impl LayoutMath for MathStyleElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut style = ctx.style;
+ if let Some(variant) = self.variant(StyleChain::default()) {
+ style = style.with_variant(variant);
+ }
+ if let Some(bold) = self.bold(StyleChain::default()) {
+ style = style.with_bold(bold);
+ }
+ if let Some(italic) = self.italic(StyleChain::default()) {
+ style = style.with_italic(italic);
+ }
+ if let Some(size) = self.size(StyleChain::default()) {
+ style = style.with_size(size);
+ }
+ if let Some(cramped) = self.cramped(StyleChain::default()) {
+ style = style.with_cramped(cramped);
+ }
+ ctx.style(style);
+ self.body().layout_math(ctx)?;
+ ctx.unstyle();
+ Ok(())
+ }
+}
+
+/// Text properties in math.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct MathStyle {
+ /// The style variant to select.
+ pub variant: MathVariant,
+ /// The size of the glyphs.
+ pub size: MathSize,
+ /// Affects the height of exponents.
+ pub cramped: bool,
+ /// Whether to use bold glyphs.
+ pub bold: bool,
+ /// Whether to use italic glyphs.
+ pub italic: Smart<bool>,
+}
+
+impl MathStyle {
+ /// This style, with the given `variant`.
+ pub fn with_variant(self, variant: MathVariant) -> Self {
+ Self { variant, ..self }
+ }
+
+ /// This style, with the given `size`.
+ pub fn with_size(self, size: MathSize) -> Self {
+ Self { size, ..self }
+ }
+
+ /// This style, with `cramped` set to the given value.
+ pub fn with_cramped(self, cramped: bool) -> Self {
+ Self { cramped, ..self }
+ }
+
+ /// This style, with `bold` set to the given value.
+ pub fn with_bold(self, bold: bool) -> Self {
+ Self { bold, ..self }
+ }
+
+ /// This style, with `italic` set to the given value.
+ pub fn with_italic(self, italic: bool) -> Self {
+ Self { italic: Smart::Custom(italic), ..self }
+ }
+
+ /// The style for subscripts in the current style.
+ pub fn for_subscript(self) -> Self {
+ self.for_superscript().with_cramped(true)
+ }
+
+ /// The style for superscripts in the current style.
+ pub fn for_superscript(self) -> Self {
+ self.with_size(match self.size {
+ MathSize::Display | MathSize::Text => MathSize::Script,
+ MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
+ })
+ }
+
+ /// The style for numerators in the current style.
+ pub fn for_numerator(self) -> Self {
+ self.with_size(match self.size {
+ MathSize::Display => MathSize::Text,
+ MathSize::Text => MathSize::Script,
+ MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
+ })
+ }
+
+ /// The style for denominators in the current style.
+ pub fn for_denominator(self) -> Self {
+ self.for_numerator().with_cramped(true)
+ }
+
+ /// Apply the style to a character.
+ pub fn styled_char(self, c: char) -> char {
+ styled_char(self, c)
+ }
+}
+
+/// The size of elements in an equation.
+///
+/// See the TeXbook p. 141.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast)]
+pub enum MathSize {
+ /// Second-level sub- and superscripts.
+ ScriptScript,
+ /// Sub- and superscripts.
+ Script,
+ /// Math in text.
+ Text,
+ /// Math on its own line.
+ Display,
+}
+
+impl MathSize {
+ pub(super) fn factor(self, ctx: &MathContext) -> f64 {
+ match self {
+ Self::Display | Self::Text => 1.0,
+ Self::Script => percent!(ctx, script_percent_scale_down),
+ Self::ScriptScript => percent!(ctx, script_script_percent_scale_down),
+ }
+ }
+}
+
+/// A mathematical style variant, as defined by Unicode.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Cast)]
+pub enum MathVariant {
+ Serif,
+ Sans,
+ Cal,
+ Frak,
+ Mono,
+ Bb,
+}
+
+impl Default for MathVariant {
+ fn default() -> Self {
+ Self::Serif
+ }
+}
+
+/// Select the correct styled math letter.
+///
+/// https://www.w3.org/TR/mathml-core/#new-text-transform-mappings
+/// https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols
+pub(super) fn styled_char(style: MathStyle, c: char) -> char {
+ use MathVariant::*;
+
+ let MathStyle { variant, bold, .. } = style;
+ let italic = style.italic.unwrap_or(matches!(
+ c,
+ 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
+ '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
+ ));
+
+ if let Some(c) = basic_exception(c) {
+ return c;
+ }
+
+ if let Some(c) = latin_exception(c, variant, bold, italic) {
+ return c;
+ }
+
+ if let Some(c) = greek_exception(c, variant, bold, italic) {
+ return c;
+ }
+
+ let base = match c {
+ 'A'..='Z' => 'A',
+ 'a'..='z' => 'a',
+ 'Α'..='Ω' => 'Α',
+ 'α'..='ω' => 'α',
+ '0'..='9' => '0',
+ _ => return c,
+ };
+
+ let tuple = (variant, bold, italic);
+ let start = match c {
+ // Latin upper.
+ 'A'..='Z' => match tuple {
+ (Serif, false, false) => 0x0041,
+ (Serif, true, false) => 0x1D400,
+ (Serif, false, true) => 0x1D434,
+ (Serif, true, true) => 0x1D468,
+ (Sans, false, false) => 0x1D5A0,
+ (Sans, true, false) => 0x1D5D4,
+ (Sans, false, true) => 0x1D608,
+ (Sans, true, true) => 0x1D63C,
+ (Cal, false, _) => 0x1D49C,
+ (Cal, true, _) => 0x1D4D0,
+ (Frak, false, _) => 0x1D504,
+ (Frak, true, _) => 0x1D56C,
+ (Mono, _, _) => 0x1D670,
+ (Bb, _, _) => 0x1D538,
+ },
+
+ // Latin lower.
+ 'a'..='z' => match tuple {
+ (Serif, false, false) => 0x0061,
+ (Serif, true, false) => 0x1D41A,
+ (Serif, false, true) => 0x1D44E,
+ (Serif, true, true) => 0x1D482,
+ (Sans, false, false) => 0x1D5BA,
+ (Sans, true, false) => 0x1D5EE,
+ (Sans, false, true) => 0x1D622,
+ (Sans, true, true) => 0x1D656,
+ (Cal, false, _) => 0x1D4B6,
+ (Cal, true, _) => 0x1D4EA,
+ (Frak, false, _) => 0x1D51E,
+ (Frak, true, _) => 0x1D586,
+ (Mono, _, _) => 0x1D68A,
+ (Bb, _, _) => 0x1D552,
+ },
+
+ // Greek upper.
+ 'Α'..='Ω' => match tuple {
+ (Serif, false, false) => 0x0391,
+ (Serif, true, false) => 0x1D6A8,
+ (Serif, false, true) => 0x1D6E2,
+ (Serif, true, true) => 0x1D71C,
+ (Sans, _, false) => 0x1D756,
+ (Sans, _, true) => 0x1D790,
+ (Cal | Frak | Mono | Bb, _, _) => return c,
+ },
+
+ // Greek lower.
+ 'α'..='ω' => match tuple {
+ (Serif, false, false) => 0x03B1,
+ (Serif, true, false) => 0x1D6C2,
+ (Serif, false, true) => 0x1D6FC,
+ (Serif, true, true) => 0x1D736,
+ (Sans, _, false) => 0x1D770,
+ (Sans, _, true) => 0x1D7AA,
+ (Cal | Frak | Mono | Bb, _, _) => return c,
+ },
+
+ // Numbers.
+ '0'..='9' => match tuple {
+ (Serif, false, _) => 0x0030,
+ (Serif, true, _) => 0x1D7CE,
+ (Bb, _, _) => 0x1D7D8,
+ (Sans, false, _) => 0x1D7E2,
+ (Sans, true, _) => 0x1D7EC,
+ (Mono, _, _) => 0x1D7F6,
+ (Cal | Frak, _, _) => return c,
+ },
+
+ _ => unreachable!(),
+ };
+
+ std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
+}
+
+fn basic_exception(c: char) -> Option<char> {
+ Some(match c {
+ '〈' => '⟨',
+ '〉' => '⟩',
+ '《' => '⟪',
+ '》' => '⟫',
+ _ => return None,
+ })
+}
+
+fn latin_exception(
+ c: char,
+ variant: MathVariant,
+ bold: bool,
+ italic: bool,
+) -> Option<char> {
+ use MathVariant::*;
+ Some(match (c, variant, bold, italic) {
+ ('B', Cal, false, _) => 'ℬ',
+ ('E', Cal, false, _) => 'ℰ',
+ ('F', Cal, false, _) => 'ℱ',
+ ('H', Cal, false, _) => 'ℋ',
+ ('I', Cal, false, _) => 'ℐ',
+ ('L', Cal, false, _) => 'ℒ',
+ ('M', Cal, false, _) => 'ℳ',
+ ('R', Cal, false, _) => 'ℛ',
+ ('C', Frak, false, _) => 'ℭ',
+ ('H', Frak, false, _) => 'ℌ',
+ ('I', Frak, false, _) => 'ℑ',
+ ('R', Frak, false, _) => 'ℜ',
+ ('Z', Frak, false, _) => 'ℨ',
+ ('C', Bb, ..) => 'ℂ',
+ ('H', Bb, ..) => 'ℍ',
+ ('N', Bb, ..) => 'ℕ',
+ ('P', Bb, ..) => 'ℙ',
+ ('Q', Bb, ..) => 'ℚ',
+ ('R', Bb, ..) => 'ℝ',
+ ('Z', Bb, ..) => 'ℤ',
+ ('h', Serif, false, true) => 'ℎ',
+ ('e', Cal, false, _) => 'ℯ',
+ ('g', Cal, false, _) => 'ℊ',
+ ('o', Cal, false, _) => 'ℴ',
+ ('ı', Serif, .., true) => '𝚤',
+ ('ȷ', Serif, .., true) => '𝚥',
+ _ => return None,
+ })
+}
+
+fn greek_exception(
+ c: char,
+ variant: MathVariant,
+ bold: bool,
+ italic: bool,
+) -> Option<char> {
+ use MathVariant::*;
+ let list = match c {
+ 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'],
+ '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'],
+ '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'],
+ 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'],
+ 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'],
+ 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'],
+ 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'],
+ 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'],
+ 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'],
+ _ => return None,
+ };
+
+ Some(match (variant, bold, italic) {
+ (Serif, true, false) => list[0],
+ (Serif, false, true) => list[1],
+ (Serif, true, true) => list[2],
+ (Sans, _, false) => list[3],
+ (Sans, _, true) => list[4],
+ _ => return None,
+ })
+}
diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs
new file mode 100644
index 00000000..796c9ebc
--- /dev/null
+++ b/crates/typst-library/src/math/underover.rs
@@ -0,0 +1,339 @@
+use super::*;
+
+const BRACE_GAP: Em = Em::new(0.25);
+const BRACKET_GAP: Em = Em::new(0.25);
+
+/// A marker to distinguish under- vs. overlines.
+enum LineKind {
+ Over,
+ Under,
+}
+
+/// A horizontal line under content.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underline(1 + 2 + ... + 5) $
+/// ```
+///
+/// Display: Underline
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderlineElem {
+ /// The content above the line.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for UnderlineElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverline(ctx, &self.body(), self.span(), LineKind::Under)
+ }
+}
+
+/// A horizontal line over content.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overline(1 + 2 + ... + 5) $
+/// ```
+///
+/// Display: Overline
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverlineElem {
+ /// The content below the line.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for OverlineElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverline(ctx, &self.body(), self.span(), LineKind::Over)
+ }
+}
+
+/// layout under- or overlined content
+fn layout_underoverline(
+ ctx: &mut MathContext,
+ body: &Content,
+ span: Span,
+ line: LineKind,
+) -> SourceResult<()> {
+ let (extra_height, content, line_pos, content_pos, baseline, bar_height);
+ match line {
+ LineKind::Under => {
+ let sep = scaled!(ctx, underbar_extra_descender);
+ bar_height = scaled!(ctx, underbar_rule_thickness);
+ let gap = scaled!(ctx, underbar_vertical_gap);
+ extra_height = sep + bar_height + gap;
+
+ content = ctx.layout_fragment(body)?;
+
+ line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
+ content_pos = Point::zero();
+ baseline = content.ascent()
+ }
+ LineKind::Over => {
+ let sep = scaled!(ctx, overbar_extra_ascender);
+ bar_height = scaled!(ctx, overbar_rule_thickness);
+ let gap = scaled!(ctx, overbar_vertical_gap);
+ extra_height = sep + bar_height + gap;
+
+ ctx.style(ctx.style.with_cramped(true));
+ content = ctx.layout_fragment(body)?;
+ ctx.unstyle();
+
+ line_pos = Point::with_y(sep + bar_height / 2.0);
+ content_pos = Point::with_y(extra_height);
+ baseline = content.ascent() + extra_height;
+ }
+ }
+
+ let width = content.width();
+ let height = content.height() + extra_height;
+ let size = Size::new(width, height);
+
+ let content_class = content.class().unwrap_or(MathClass::Normal);
+ let mut frame = Frame::new(size);
+ frame.set_baseline(baseline);
+ frame.push_frame(content_pos, content.into_frame());
+ frame.push(
+ line_pos,
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(width)).stroked(Stroke {
+ paint: TextElem::fill_in(ctx.styles()),
+ thickness: bar_height,
+ ..Stroke::default()
+ }),
+ span,
+ ),
+ );
+
+ ctx.push(FrameFragment::new(ctx, frame).with_class(content_class));
+
+ Ok(())
+}
+
+/// A horizontal brace under content, with an optional annotation below.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Underbrace
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderbraceElem {
+ /// The content above the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for UnderbraceElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⏟',
+ BRACE_GAP,
+ false,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal brace over content, with an optional annotation above.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Overbrace
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverbraceElem {
+ /// The content below the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for OverbraceElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⏞',
+ BRACE_GAP,
+ true,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal bracket under content, with an optional annotation below.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Underbracket
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderbracketElem {
+ /// The content above the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for UnderbracketElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⎵',
+ BRACKET_GAP,
+ false,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal bracket over content, with an optional annotation above.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Overbracket
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverbracketElem {
+ /// The content below the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for OverbracketElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⎴',
+ BRACKET_GAP,
+ true,
+ self.span(),
+ )
+ }
+}
+
+/// Layout an over- or underbrace-like object.
+fn layout_underoverspreader(
+ ctx: &mut MathContext,
+ body: &Content,
+ annotation: &Option<Content>,
+ c: char,
+ gap: Em,
+ reverse: bool,
+ span: Span,
+) -> SourceResult<()> {
+ let gap = gap.scaled(ctx);
+ let body = ctx.layout_row(body)?;
+ let body_class = body.class();
+ let body = body.into_fragment(ctx);
+ let glyph = GlyphFragment::new(ctx, c, span);
+ let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
+
+ let mut rows = vec![MathRow::new(vec![body]), stretched.into()];
+ ctx.style(if reverse {
+ ctx.style.for_subscript()
+ } else {
+ ctx.style.for_superscript()
+ });
+ rows.extend(
+ annotation
+ .as_ref()
+ .map(|annotation| ctx.layout_row(annotation))
+ .transpose()?,
+ );
+ ctx.unstyle();
+
+ let mut baseline = 0;
+ if reverse {
+ rows.reverse();
+ baseline = rows.len() - 1;
+ }
+
+ let frame = stack(ctx, rows, Align::Center, gap, baseline);
+ ctx.push(FrameFragment::new(ctx, frame).with_class(body_class));
+
+ Ok(())
+}
+
+/// Stack rows on top of each other.
+///
+/// Add a `gap` between each row and uses the baseline of the `baseline`th
+/// row for the whole frame.
+pub(super) fn stack(
+ ctx: &MathContext,
+ rows: Vec<MathRow>,
+ align: Align,
+ gap: Abs,
+ baseline: usize,
+) -> Frame {
+ let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
+ let AlignmentResult { points, width } = alignments(&rows);
+ let rows: Vec<_> = rows
+ .into_iter()
+ .map(|row| row.into_aligned_frame(ctx, &points, align))
+ .collect();
+
+ let mut y = Abs::zero();
+ let mut frame = Frame::new(Size::new(
+ width,
+ rows.iter().map(|row| row.height()).sum::<Abs>()
+ + rows.len().saturating_sub(1) as f64 * gap,
+ ));
+
+ for (i, row) in rows.into_iter().enumerate() {
+ let x = align.position(width - row.width());
+ let pos = Point::new(x, y);
+ if i == baseline {
+ frame.set_baseline(y + row.baseline());
+ }
+ y += row.height() + gap;
+ frame.push_frame(pos, row);
+ }
+
+ frame
+}