summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-11-23 16:25:49 +0100
committerLaurenz <laurmaedje@gmail.com>2023-11-24 12:30:02 +0100
commit7eebafa7837ec173a7b2064ae60fd45b5413d17c (patch)
treeb63b302b6d7747bcbb28571713745b9ca1aa83a4 /crates/typst-library/src/math
parent76e173b78b511b506b928c27ac360f75fa455747 (diff)
Merge `typst` and `typst-library`
Diffstat (limited to 'crates/typst-library/src/math')
-rw-r--r--crates/typst-library/src/math/accent.rs137
-rw-r--r--crates/typst-library/src/math/align.rs60
-rw-r--r--crates/typst-library/src/math/attach.rs440
-rw-r--r--crates/typst-library/src/math/cancel.rs230
-rw-r--r--crates/typst-library/src/math/class.rs38
-rw-r--r--crates/typst-library/src/math/ctx.rs335
-rw-r--r--crates/typst-library/src/math/frac.rs157
-rw-r--r--crates/typst-library/src/math/fragment.rs513
-rw-r--r--crates/typst-library/src/math/lr.rs195
-rw-r--r--crates/typst-library/src/math/matrix.rs655
-rw-r--r--crates/typst-library/src/math/mod.rs500
-rw-r--r--crates/typst-library/src/math/op.rs115
-rw-r--r--crates/typst-library/src/math/root.rs137
-rw-r--r--crates/typst-library/src/math/row.rs261
-rw-r--r--crates/typst-library/src/math/spacing.rs63
-rw-r--r--crates/typst-library/src/math/stretch.rs199
-rw-r--r--crates/typst-library/src/math/style.rs574
-rw-r--r--crates/typst-library/src/math/underover.rs315
18 files changed, 0 insertions, 4924 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs
deleted file mode 100644
index 1b2d4793..00000000
--- a/crates/typst-library/src/math/accent.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-use crate::math::*;
-
-/// 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
-/// $grave(a) = accent(a, `)$ \
-/// $arrow(a) = accent(a, arrow)$ \
-/// $tilde(a) = accent(a, \u{0303})$
-/// ```
-#[elem(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::soft(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.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-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().clone().into()).cast()?,
- None => bail!("expected text"),
- },
-}
diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs
deleted file mode 100644
index 4192e97b..00000000
--- a/crates/typst-library/src/math/align.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use crate::math::*;
-
-/// A math alignment point: `&`, `&&`.
-#[elem(title = "Alignment Point", 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
deleted file mode 100644
index 3e6b69f2..00000000
--- a/crates/typst-library/src/math/attach.rs
+++ /dev/null
@@ -1,440 +0,0 @@
-use super::*;
-
-/// A base with optional attachments.
-///
-/// ```example
-/// $ attach(
-/// Pi, t: alpha, b: beta,
-/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
-/// ) $
-/// ```
-#[elem(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])
- }
-}
-
-/// Grouped primes.
-///
-/// ```example
-/// $ a'''_b = a^'''_b $
-/// ```
-///
-/// # Syntax
-/// This function has dedicated syntax: use apostrophes instead of primes. They
-/// will automatically attach to the previous element, moving superscripts to
-/// the next level.
-#[elem(LayoutMath)]
-pub struct PrimesElem {
- /// The number of grouped primes.
- #[required]
- pub count: usize,
-}
-
-impl LayoutMath for PrimesElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- match *self.count() {
- count @ 1..=4 => {
- let f = ctx.layout_fragment(&TextElem::packed(match count {
- 1 => '′',
- 2 => '″',
- 3 => '‴',
- 4 => '⁗',
- _ => unreachable!(),
- }))?;
- ctx.push(f);
- }
- count => {
- // Custom amount of primes
- let prime = ctx.layout_fragment(&TextElem::packed('′'))?.into_frame();
- let width = prime.width() * (count + 1) as f64 / 2.0;
- let mut frame = Frame::soft(Size::new(width, prime.height()));
- frame.set_baseline(prime.ascent());
-
- for i in 0..count {
- frame.push_frame(
- Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
- prime.clone(),
- )
- }
- ctx.push(FrameFragment::new(ctx, frame));
- }
- }
- Ok(())
- }
-}
-
-/// Forces a base to display attachments as scripts.
-///
-/// ```example
-/// $ scripts(sum)_1^2 != sum_1^2 $
-/// ```
-#[elem(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
-/// $ limits(A)_1^2 != A_1^2 $
-/// ```
-#[elem(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 {
- match unicode_math_class::class(c) {
- Some(MathClass::Large) => {
- if is_integral_char(c) {
- Limits::Never
- } else {
- Limits::Display
- }
- }
- Some(MathClass::Relation) => Limits::Always,
- _ => 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,
- }
- }
-}
-
-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::soft(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::soft(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)
-}
-
-/// Determines if the character is one of a variety of integral signs
-fn is_integral_char(c: char) -> bool {
- ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
-}
-
-/// 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
deleted file mode 100644
index 455750f7..00000000
--- a/crates/typst-library/src/math/cancel.rs
+++ /dev/null
@@ -1,230 +0,0 @@
-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
-/// >>> #set page(width: 140pt)
-/// Here, we can simplify:
-/// $ (a dot b dot cancel(x)) /
-/// cancel(x) $
-/// ```
-#[elem(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>,
-
- /// Whether the cancel line should be inverted (flipped along the y-axis).
- /// For the default angle setting, inverted means the cancel line
- /// points to the top left instead of top right.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ (a cancel((b + c), inverted: #true)) /
- /// cancel(b + c, inverted: #true) $
- /// ```
- #[default(false)]
- pub inverted: bool,
-
- /// Whether two opposing cancel lines should be drawn, forming a cross over
- /// the element. Overrides `inverted`.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(Pi, cross: #true) $
- /// ```
- #[default(false)]
- pub cross: bool,
-
- /// How much to rotate the cancel line.
- ///
- /// - If `{auto}`, the line assumes the default angle; that is, along the
- /// diagonal line of the content box.
- /// - If given an angle, the line is rotated by that angle clockwise w.r.t
- /// the y-axis.
- /// - If given a function `angle => angle`, the line is rotated by the angle
- /// returned by that function. The function receives the default angle as
- /// its input.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(Pi)
- /// cancel(Pi, angle: #0deg)
- /// cancel(Pi, angle: #45deg)
- /// cancel(Pi, angle: #90deg)
- /// cancel(1/(1+x), angle: #(a => a + 45deg))
- /// cancel(1/(1+x), angle: #(a => a + 90deg)) $
- /// ```
- pub angle: Smart<CancelAngle>,
-
- /// How to [stroke]($stroke) the cancel line.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(
- /// sum x,
- /// stroke: #(
- /// paint: red,
- /// thickness: 1.5pt,
- /// dash: "dashed",
- /// ),
- /// ) $
- /// ```
- #[resolve]
- #[fold]
- #[default(Stroke {
- // Default stroke has 0.5pt for better visuals.
- thickness: Smart::Custom(Abs::pt(0.5)),
- ..Default::default()
- })]
- pub stroke: Stroke,
-}
-
-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(FixedStroke {
- paint: TextElem::fill_in(styles).as_decoration(),
- ..Default::default()
- });
-
- let invert = self.inverted(styles);
- let cross = self.cross(styles);
- let angle = self.angle(styles);
-
- let invert_first_line = !cross && invert;
- let first_line = draw_cancel_line(
- ctx,
- 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(ctx, length, stroke, true, &angle, body_size, span)?;
-
- body.push_frame(center, second_line);
- }
-
- ctx.push(FrameFragment::new(ctx, body).with_class(body_class));
-
- Ok(())
- }
-}
-
-/// Defines the cancel line.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum CancelAngle {
- Angle(Angle),
- Func(Func),
-}
-
-cast! {
- CancelAngle,
- self => match self {
- Self::Angle(v) => v.into_value(),
- Self::Func(v) => v.into_value()
- },
- v: Angle => CancelAngle::Angle(v),
- v: Func => CancelAngle::Func(v),
-}
-
-/// Draws a cancel line.
-fn draw_cancel_line(
- ctx: &mut MathContext,
- length_scale: Rel<Abs>,
- stroke: FixedStroke,
- invert: bool,
- angle: &Smart<CancelAngle>,
- body_size: Size,
- span: Span,
-) -> SourceResult<Frame> {
- let default = default_angle(body_size);
- let mut angle = match angle {
- // Non specified angle defaults to the diagonal
- Smart::Auto => default,
- Smart::Custom(angle) => match angle {
- // This specifies the absolute angle w.r.t y-axis clockwise.
- CancelAngle::Angle(v) => *v,
- // This specifies a function that takes the default angle as input.
- CancelAngle::Func(func) => {
- func.call_vt(ctx.vt, [default])?.cast().at(span)?
- }
- },
- };
-
- // invert means flipping along the y-axis
- if invert {
- angle *= -1.0;
- }
-
- // same as above, the default length is the diagonal of the body box.
- let default_length = body_size.to_point().hypot();
- let length = length_scale.relative_to(default_length);
-
- // Draw a vertical line of length and rotate it by angle
- let start = Point::new(Abs::zero(), length / 2.0);
- let delta = Point::new(Abs::zero(), -length);
-
- let mut frame = Frame::soft(body_size);
- frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span));
-
- // Having the middle of the line at the origin is convenient here.
- frame.transform(Transform::rotate(angle));
- Ok(frame)
-}
-
-/// The default line angle for a body of the given size.
-fn default_angle(body: Size) -> Angle {
- // The default cancel line is the diagonal.
- // We infer the default angle from
- // the diagonal w.r.t to the body box.
- //
- // The returned angle is in the range of [0, Pi/2]
- //
- // Note that the angle is computed w.r.t to the y-axis
- //
- // B
- // /|
- // diagonal / | height
- // / |
- // / |
- // O ----
- // width
- let (width, height) = (body.x, body.y);
- let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2])
- Angle::rad(default_angle)
-}
diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs
deleted file mode 100644
index d2c5192d..00000000
--- a/crates/typst-library/src/math/class.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use super::*;
-
-/// Forced use of a certain math class.
-///
-/// This is useful to treat certain symbols as if they were of a different
-/// class, e.g. to make a symbol behave like a relation.
-///
-/// # Example
-/// ```example
-/// #let loves = math.class(
-/// "relation",
-/// sym.suit.heart,
-/// )
-///
-/// $x loves y and y loves 5$
-/// ```
-#[elem(LayoutMath)]
-pub struct ClassElem {
- /// The class to apply to the content.
- #[required]
- pub class: MathClass,
-
- /// The content to which the class is applied.
- #[required]
- pub body: Content,
-}
-
-impl LayoutMath for ClassElem {
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- ctx.style(ctx.style.with_class(*self.class()));
- let mut fragment = ctx.layout_fragment(self.body())?;
- ctx.unstyle();
-
- fragment.set_class(*self.class());
- ctx.push(fragment);
- Ok(())
- }
-}
diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs
deleted file mode 100644
index 789bd332..00000000
--- a/crates/typst-library/src/math/ctx.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-use comemo::Prehashed;
-use ttf_parser::gsub::SubstitutionSubtable;
-use ttf_parser::math::MathValue;
-use typst::font::{FontStyle, FontWeight};
-use typst::model::realize;
-use typst::syntax::is_newline;
-use unicode_segmentation::UnicodeSegmentation;
-
-use super::*;
-use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric};
-
-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 glyphwise_tables: Option<Vec<GlyphwiseSubsts<'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 math_table = font.ttf().tables().math.unwrap();
- let gsub_table = font.ttf().tables().gsub;
- let constants = math_table.constants.unwrap();
-
- let ssty_table = gsub_table
- .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::<SubstitutionSubtable>(0))
- .and_then(|ssty| match ssty {
- SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs),
- _ => None,
- });
-
- let features = tags(styles);
- let glyphwise_tables = gsub_table.map(|gsub| {
- features
- .into_iter()
- .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
- .collect()
- });
-
- 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: math_table,
- constants,
- ssty_table,
- glyphwise_tables,
- space_width,
- fragments: vec![],
- local: Styles::new(),
- style: MathStyle {
- variant: MathVariant::Serif,
- size: if block { MathSize::Display } else { MathSize::Text },
- class: Smart::Auto,
- 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_box(&mut self, boxed: &BoxElem) -> SourceResult<Frame> {
- Ok(boxed
- .layout(self.vt, self.outer.chain(&self.local), self.regions)?
- .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::Script => {
- glyph.make_scriptsize(self);
- }
- MathSize::ScriptScript => {
- glyph.make_scriptscriptsize(self);
- }
- _ => (),
- }
-
- let class = self.style.class.as_custom().or(glyph.class);
- if class == Some(MathClass::Large) {
- let mut variant = if self.style.size == MathSize::Display {
- let height = scaled!(self, display_operator_min_height);
- glyph.stretch_vertical(self, height, Abs::zero())
- } else {
- glyph.into_variant()
- };
- // TeXbook p 155. Large operators are always vertically centered on the axis.
- variant.center_on_axis(self);
- variant.into()
- } else {
- glyph.into()
- }
- } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
- // 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 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();
- if text.contains(is_newline) {
- let mut fragments = vec![];
- for (i, piece) in text.split(is_newline).enumerate() {
- if i != 0 {
- fragments.push(MathFragment::Linebreak);
- }
- if !piece.is_empty() {
- fragments.push(self.layout_complex_text(piece, span)?.into());
- }
- }
- let mut frame = MathRow::new(fragments).into_frame(self);
- let axis = scaled!(self, axis_height);
- frame.set_baseline(frame.height() / 2.0 + axis);
- FrameFragment::new(self, frame).into()
- } else {
- self.layout_complex_text(&text, span)?.into()
- }
- };
- Ok(fragment)
- }
-
- pub fn layout_complex_text(
- &mut self,
- text: &str,
- span: Span,
- ) -> SourceResult<FrameFragment> {
- let spaced = text.graphemes(true).nth(1).is_some();
- let elem = TextElem::packed(text)
- .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)))
- .styled(TextElem::set_bottom_edge(BottomEdge::Metric(
- BottomEdgeMetric::Bounds,
- )))
- .spanned(span);
-
- // There isn't a natural width for a paragraph in a math environment;
- // because it will be placed somewhere probably not at the left margin
- // it will overflow. So emulate an `hbox` instead and allow the paragraph
- // to extend as far as needed.
- let span = elem.span();
- let frame = ParElem::new(vec![Prehashed::new(elem)])
- .spanned(span)
- .layout(
- self.vt,
- self.outer.chain(&self.local),
- false,
- Size::splat(Abs::inf()),
- false,
- )?
- .into_frame();
-
- Ok(FrameFragment::new(self, frame)
- .with_class(MathClass::Alphabetic)
- .with_spaced(spaced))
- }
-
- 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 {
- // The normal weight is what we started with.
- // It's 400 for CM Regular, 450 for CM Book.
- self.font.info().variant.weight
- }));
- 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/frac.rs b/crates/typst-library/src/math/frac.rs
deleted file mode 100644
index c3014178..00000000
--- a/crates/typst-library/src/math/frac.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-use super::*;
-
-const FRAC_AROUND: Em = Em::new(0.1);
-
-/// A mathematical fraction.
-///
-/// # Example
-/// ```example
-/// $ 1/2 < (x+1)/2 $
-/// $ ((x+1)) / 2 = frac(a, b) $
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: Use a slash to turn neighbouring
-/// expressions into a fraction. Multiple atoms can be grouped into a single
-/// expression using round grouping parenthesis. Such parentheses are removed
-/// from the output, but you can nest multiple to force them.
-#[elem(title = "Fraction", 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(), std::slice::from_ref(self.denom()), false, self.span())
- }
-}
-
-/// A binomial expression.
-///
-/// # Example
-/// ```example
-/// $ binom(n, k) $
-/// $ binom(n, k_1, k_2, k_3, ..., k_m) $
-/// ```
-#[elem(title = "Binomial", LayoutMath)]
-pub struct BinomElem {
- /// The binomial's upper index.
- #[required]
- pub upper: Content,
-
- /// The binomial's lower index.
- #[required]
- #[variadic]
- #[parse(
- let values = args.all::<Spanned<Value>>()?;
- if values.is_empty() {
- // Prevents one element binomials
- bail!(args.span, "missing argument: lower");
- }
- values.into_iter().map(|spanned| spanned.v.display()).collect()
- )]
- pub lower: Vec<Content>,
-}
-
-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(&Content::sequence(
- // Add a comma between each element.
- denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1),
- ))?;
- 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::soft(size);
- frame.set_baseline(baseline);
- frame.push_frame(num_pos, num);
- frame.push_frame(denom_pos, denom);
-
- if binom {
- let mut left =
- GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall);
- left.center_on_axis(ctx);
- ctx.push(left);
- ctx.push(FrameFragment::new(ctx, frame));
- let mut right =
- GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall);
- right.center_on_axis(ctx);
- ctx.push(right);
- } else {
- frame.push(
- line_pos,
- FrameItem::Shape(
- Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness,
- ..FixedStroke::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
deleted file mode 100644
index 76ee2512..00000000
--- a/crates/typst-library/src/math/fragment.rs
+++ /dev/null
@@ -1,513 +0,0 @@
-use rustybuzz::Feature;
-use ttf_parser::gsub::{
- AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
-};
-use ttf_parser::opentype_layout::LayoutTable;
-
-use super::*;
-
-#[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> {
- self.style().and_then(|style| style.class.as_custom()).or(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) {
- macro_rules! set_style_class {
- ($fragment:ident) => {
- if $fragment.style.class.is_custom() {
- $fragment.style.class = Smart::Custom(class);
- }
- };
- }
-
- match self {
- Self::Glyph(glyph) => {
- glyph.class = Some(class);
- set_style_class!(glyph);
- }
- Self::Variant(variant) => {
- variant.class = Some(class);
- set_style_class!(variant);
- }
- Self::Frame(fragment) => {
- fragment.class = class;
- set_style_class!(fragment);
- }
- _ => {}
- }
- }
-
- 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) => {
- match self.style().and_then(|style| style.class.as_custom()) {
- Some(MathClass::Fence) => true,
- Some(_) => false,
- None => 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::soft(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 shift: Abs,
- 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: SmallVec<[Meta; 1]>,
- 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();
- let id = Self::adjust_glyph_index(ctx, id);
- 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)?;
- let id = Self::adjust_glyph_index(ctx, id);
- 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),
- '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
- _ => 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()).as_decoration(),
- shift: TextElem::baseline_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
- }
-
- /// Apply GSUB substitutions.
- fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId {
- if let Some(glyphwise_tables) = &ctx.glyphwise_tables {
- glyphwise_tables.iter().fold(id, |id, table| table.apply(id))
- } else {
- id
- }
- }
-
- /// 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::soft(size);
- frame.set_baseline(self.ascent);
- frame.push(Point::with_y(self.ascent + self.shift), 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 VariantFragment {
- /// Vertically adjust the fragment's frame so that it is centered
- /// on the axis.
- pub fn center_on_axis(&mut self, ctx: &MathContext) {
- let h = self.frame.height();
- self.frame.set_baseline(h / 2.0 + scaled!(ctx, axis_height));
- }
-}
-
-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))
-}
-
-/// An OpenType substitution table that is applicable to glyph-wise substitutions.
-pub enum GlyphwiseSubsts<'a> {
- Single(SingleSubstitution<'a>),
- Alternate(AlternateSubstitution<'a>, u32),
-}
-
-impl<'a> GlyphwiseSubsts<'a> {
- pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> {
- let table = gsub
- .features
- .find(ttf_parser::Tag(feature.tag.0))
- .and_then(|feature| feature.lookup_indices.get(0))
- .and_then(|index| gsub.lookups.get(index))?;
- let table = table.subtables.get::<SubstitutionSubtable>(0)?;
- match table {
- SubstitutionSubtable::Single(single_glyphs) => {
- Some(Self::Single(single_glyphs))
- }
- SubstitutionSubtable::Alternate(alt_glyphs) => {
- Some(Self::Alternate(alt_glyphs, feature.value))
- }
- _ => None,
- }
- }
-
- pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
- match self {
- Self::Single(single) => match single {
- SingleSubstitution::Format1 { coverage, delta } => coverage
- .get(glyph_id)
- .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
- SingleSubstitution::Format2 { coverage, substitutes } => {
- coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
- }
- },
- Self::Alternate(alternate, value) => alternate
- .coverage
- .get(glyph_id)
- .and_then(|idx| alternate.alternate_sets.get(idx))
- .and_then(|set| set.alternates.get(*value as u16)),
- }
- }
-
- pub fn apply(&self, glyph_id: GlyphId) -> GlyphId {
- self.try_apply(glyph_id).unwrap_or(glyph_id)
- }
-}
diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs
deleted file mode 100644
index 39143620..00000000
--- a/crates/typst-library/src/math/lr.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-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.
-#[elem(title = "Left/Right", 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);
- let mut stretched = glyph.stretch_vertical(ctx, height, short_fall);
- stretched.center_on_axis(ctx);
-
- *fragment = MathFragment::Variant(stretched);
- if let Some(class) = apply {
- fragment.set_class(class);
- }
- }
-}
-
-/// Floors an expression.
-///
-/// ```example
-/// $ floor(x/2) $
-/// ```
-#[func]
-pub fn floor(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to floor.
- body: Content,
-) -> Content {
- delimited(body, '⌊', '⌋', size)
-}
-
-/// Ceils an expression.
-///
-/// ```example
-/// $ ceil(x/2) $
-/// ```
-#[func]
-pub fn ceil(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to ceil.
- body: Content,
-) -> Content {
- delimited(body, '⌈', '⌉', size)
-}
-
-/// Rounds an expression.
-///
-/// ```example
-/// $ round(x/2) $
-/// ```
-#[func]
-pub fn round(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to round.
- body: Content,
-) -> Content {
- delimited(body, '⌊', '⌉', size)
-}
-
-/// Takes the absolute value of an expression.
-///
-/// ```example
-/// $ abs(x/2) $
-/// ```
-#[func]
-pub fn abs(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to take the absolute value of.
- body: Content,
-) -> Content {
- delimited(body, '|', '|', size)
-}
-
-/// Takes the norm of an expression.
-///
-/// ```example
-/// $ norm(x/2) $
-/// ```
-#[func]
-pub fn norm(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to take the norm of.
- body: Content,
-) -> Content {
- delimited(body, '‖', '‖', size)
-}
-
-fn delimited(
- body: Content,
- left: char,
- right: char,
- size: Option<Smart<Rel<Length>>>,
-) -> Content {
- let mut elem = LrElem::new(Content::sequence([
- TextElem::packed(left),
- body,
- TextElem::packed(right),
- ]));
- // Push size only if size is provided
- if let Some(size) = size {
- elem.push_size(size);
- }
- elem.pack()
-}
diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs
deleted file mode 100644
index b5d21ed6..00000000
--- a/crates/typst-library/src/math/matrix.rs
+++ /dev/null
@@ -1,655 +0,0 @@
-use super::*;
-
-const DEFAULT_ROW_GAP: Em = Em::new(0.5);
-const DEFAULT_COL_GAP: Em = Em::new(0.5);
-const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
-
-const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
-
-/// A column vector.
-///
-/// Content in the vector's elements can be aligned with the `&` symbol.
-///
-/// # Example
-/// ```example
-/// $ vec(a, b, c) dot vec(1, 2, 3)
-/// = a + 2b + 3c $
-/// ```
-#[elem(title = "Vector", 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 gap between elements.
- ///
- /// ```example
- /// #set math.vec(gap: 1em)
- /// $ vec(1, 2) $
- /// ```
- #[resolve]
- #[default(DEFAULT_ROW_GAP.into())]
- pub gap: Rel<Length>,
-
- /// 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(),
- FixedAlign::Center,
- self.gap(ctx.styles()),
- )?;
- 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
-/// $ mat(
-/// 1, 2, ..., 10;
-/// 2, 2, ..., 10;
-/// dots.v, dots.v, dots.down, dots.v;
-/// 10, 10, ..., 10;
-/// ) $
-/// ```
-#[elem(title = "Matrix", 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>,
-
- /// Draws augmentation lines in a matrix.
- ///
- /// - `{none}`: No lines are drawn.
- /// - A single number: A vertical augmentation line is drawn
- /// after the specified column number. Negative numbers start from the end.
- /// - A dictionary: With a dictionary, multiple augmentation lines can be
- /// drawn both horizontally and vertically. Additionally, the style of the
- /// lines can be set. The dictionary can contain the following keys:
- /// - `hline`: The offsets at which horizontal lines should be drawn.
- /// For example, an offset of `2` would result in a horizontal line
- /// being drawn after the second row of the matrix. Accepts either an
- /// integer for a single line, or an array of integers
- /// for multiple lines. Like for a single number, negative numbers start from the end.
- /// - `vline`: The offsets at which vertical lines should be drawn.
- /// For example, an offset of `2` would result in a vertical line being
- /// drawn after the second column of the matrix. Accepts either an
- /// integer for a single line, or an array of integers
- /// for multiple lines. Like for a single number, negative numbers start from the end.
- /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`,
- /// takes on a thickness of 0.05em and square line caps.
- ///
- /// ```example
- /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $
- /// // Equivalent to:
- /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $
- /// ```
- ///
- /// ```example
- /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $
- /// ```
- #[resolve]
- #[fold]
- pub augment: Option<Augment>,
-
- /// The gap between rows and columns.
- ///
- /// ```example
- /// #set math.mat(gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[external]
- pub gap: Rel<Length>,
-
- /// The gap between rows. Takes precedence over `gap`.
- ///
- /// ```example
- /// #set math.mat(row-gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[resolve]
- #[parse(
- let gap = args.named("gap")?;
- args.named("row-gap")?.or(gap)
- )]
- #[default(DEFAULT_ROW_GAP.into())]
- pub row_gap: Rel<Length>,
-
- /// The gap between columns. Takes precedence over `gap`.
- ///
- /// ```example
- /// #set math.mat(column-gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[resolve]
- #[parse(args.named("column-gap")?.or(gap))]
- #[default(DEFAULT_COL_GAP.into())]
- pub column_gap: Rel<Length>,
-
- /// An array of arrays with the rows of the matrix.
- ///
- /// ```example
- /// #let data = ((1, 2, 3), (4, 5, 6))
- /// #let matrix = math.mat(..data)
- /// $ v := matrix $
- /// ```
- #[variadic]
- #[parse(
- let mut rows = vec![];
- let mut width = 0;
-
- let values = args.all::<Spanned<Value>>()?;
- if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) {
- for Spanned { v, span } in values {
- let array = v.cast::<Array>().at(span)?;
- let row: Vec<_> = array.into_iter().map(Value::display).collect();
- width = width.max(row.len());
- rows.push(row);
- }
- } else {
- rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()];
- }
-
- for row in &mut rows {
- if row.len() < width {
- row.resize(width, Content::empty());
- }
- }
-
- rows
- )]
- pub rows: Vec<Vec<Content>>,
-}
-
-impl LayoutMath for MatElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- // validate inputs
-
- let augment = self.augment(ctx.styles());
- let rows = self.rows();
-
- if let Some(aug) = &augment {
- for &offset in &aug.hline.0 {
- if offset == 0 || offset.unsigned_abs() >= rows.len() {
- bail!(
- self.span(),
- "cannot draw a horizontal line after row {} of a matrix with {} rows",
- if offset < 0 { rows.len() as isize + offset } else { offset },
- rows.len()
- );
- }
- }
-
- let ncols = self.rows().first().map_or(0, |row| row.len());
-
- for &offset in &aug.vline.0 {
- if offset == 0 || offset.unsigned_abs() >= ncols {
- bail!(
- self.span(),
- "cannot draw a vertical line after column {} of a matrix with {} columns",
- if offset < 0 { ncols as isize + offset } else { offset },
- ncols
- );
- }
- }
- }
-
- let delim = self.delim(ctx.styles());
- let frame = layout_mat_body(
- ctx,
- rows,
- augment,
- Axes::new(self.column_gap(ctx.styles()), self.row_gap(ctx.styles())),
- self.span(),
- )?;
-
- 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
-/// $ f(x, y) := cases(
-/// 1 "if" (x dot y)/2 <= 0,
-/// 2 "if" x "is even",
-/// 3 "if" x in NN,
-/// 4 "else",
-/// ) $
-/// ```
-#[elem(LayoutMath)]
-pub struct CasesElem {
- /// The delimiter to use.
- ///
- /// ```example
- /// #set math.cases(delim: "[")
- /// $ x = cases(1, 2) $
- /// ```
- #[default(Delimiter::Brace)]
- pub delim: Delimiter,
-
- /// Whether the direction of cases should be reversed.
- ///
- /// ```example
- /// #set math.cases(reverse: true)
- /// $ cases(1, 2) = x $
- /// ```
- #[default(false)]
- pub reverse: bool,
-
- /// The gap between branches.
- ///
- /// ```example
- /// #set math.cases(gap: 1em)
- /// $ x = cases(1, 2) $
- /// ```
- #[resolve]
- #[default(DEFAULT_ROW_GAP.into())]
- pub gap: Rel<Length>,
-
- /// 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(),
- FixedAlign::Start,
- self.gap(ctx.styles()),
- )?;
-
- let (open, close) = if self.reverse(ctx.styles()) {
- (None, Some(delim.close()))
- } else {
- (Some(delim.open()), None)
- };
-
- layout_delimiters(ctx, frame, open, close, 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: FixedAlign,
- row_gap: Rel<Abs>,
-) -> SourceResult<Frame> {
- let gap = row_gap.relative_to(ctx.regions.base().y);
- 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>],
- augment: Option<Augment<Abs>>,
- gap: Axes<Rel<Abs>>,
- span: Span,
-) -> SourceResult<Frame> {
- let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to);
- let half_gap = gap * 0.5;
-
- // We provide a default stroke thickness that scales
- // with font size to ensure that augmentation lines
- // look correct by default at all matrix sizes.
- // The line cap is also set to square because it looks more "correct".
- let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx);
- let default_stroke = FixedStroke {
- thickness: default_stroke_thickness,
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- line_cap: LineCap::Square,
- ..Default::default()
- };
-
- let (hline, vline, stroke) = match augment {
- Some(v) => {
- // need to get stroke here for ownership
- let stroke = v.stroke_or(default_stroke);
-
- (v.hline, v.vline, stroke)
- }
- _ => (Offsets::default(), Offsets::default(), default_stroke),
- };
-
- let ncols = rows.first().map_or(0, |row| row.len());
- let nrows = rows.len();
- if ncols == 0 || nrows == 0 {
- return Ok(Frame::soft(Size::zero()));
- }
-
- // Before the full matrix body can be laid out, the
- // individual cells must first be independently laid out
- // so we can ensure alignment across rows and columns.
-
- // This variable stores the maximum ascent and descent for each row.
- let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
-
- // We want to transpose our data layout to columns
- // before final layout. For efficiency, the columns
- // variable is set up here and newly generated
- // individual cells are then added to it.
- let mut cols = vec![vec![]; ncols];
-
- ctx.style(ctx.style.for_denominator());
- 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();
-
- // For each row, combine maximum ascent and descent into a row height.
- // Sum the row heights, then add the total height of the gaps between rows.
- let total_height =
- heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64;
-
- // Width starts at zero because it can't be calculated until later
- let mut frame = Frame::soft(Size::new(Abs::zero(), total_height));
-
- let mut x = Abs::zero();
-
- for (index, col) in cols.into_iter().enumerate() {
- 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, FixedAlign::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 + gap.y;
- }
-
- // Advance to the end of the column
- x += rcol;
-
- // If a vertical line should be inserted after this column
- if vline.0.contains(&(index as isize + 1))
- || vline.0.contains(&(1 - ((ncols - index) as isize)))
- {
- frame.push(
- Point::with_x(x + half_gap.x),
- line_item(total_height, true, stroke.clone(), span),
- );
- }
-
- // Advance to the start of the next column
- x += gap.x;
- }
-
- // Once all the columns are laid out, the total width can be calculated
- let total_width = x - gap.x;
-
- // This allows the horizontal lines to be laid out
- for line in hline.0 {
- let real_line =
- if line < 0 { nrows - line.unsigned_abs() } else { line as usize };
- let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>()
- + gap.y * (real_line - 1) as f64)
- + half_gap.y;
-
- frame.push(
- Point::with_y(offset),
- line_item(total_width, false, stroke.clone(), span),
- );
- }
-
- frame.size_mut().x = total_width;
-
- Ok(frame)
-}
-
-fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem {
- let line_geom = if vertical {
- Geometry::Line(Point::with_y(length))
- } else {
- Geometry::Line(Point::with_x(length))
- };
-
- FrameItem::Shape(
- Shape {
- geometry: line_geom,
- fill: None,
- stroke: Some(stroke),
- },
- span,
- )
-}
-
-/// Layout the outer wrapper around the body of a vector or matrix.
-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 {
- let mut left =
- GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall);
- left.center_on_axis(ctx);
- ctx.push(left);
- }
-
- ctx.push(FrameFragment::new(ctx, frame));
-
- if let Some(right) = right {
- let mut right = GlyphFragment::new(ctx, right, span)
- .stretch_vertical(ctx, target, short_fall);
- right.center_on_axis(ctx);
- ctx.push(right);
- }
-
- Ok(())
-}
-
-/// Parameters specifying how augmentation lines
-/// should be drawn on a matrix.
-#[derive(Debug, Default, Clone, PartialEq, Hash)]
-pub struct Augment<T: Numeric = Length> {
- pub hline: Offsets,
- pub vline: Offsets,
- pub stroke: Smart<Stroke<T>>,
-}
-
-impl Augment<Abs> {
- fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke {
- match &self.stroke {
- Smart::Custom(v) => v.clone().unwrap_or(fallback),
- Smart::Auto => fallback,
- }
- }
-}
-
-impl Resolve for Augment {
- type Output = Augment<Abs>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- Augment {
- hline: self.hline,
- vline: self.vline,
- stroke: self.stroke.resolve(styles),
- }
- }
-}
-
-impl Fold for Augment<Abs> {
- type Output = Augment<Abs>;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- // Special case for handling `auto` strokes in subsequent `Augment`.
- if self.stroke.is_auto() && outer.stroke.is_custom() {
- self.stroke = outer.stroke;
- } else {
- self.stroke = self.stroke.fold(outer.stroke);
- }
-
- self
- }
-}
-
-cast! {
- Augment,
- self => {
- // if the stroke is auto and there is only one vertical line,
- if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 {
- return self.vline.0[0].into_value();
- }
-
- let d = dict! {
- "hline" => self.hline.into_value(),
- "vline" => self.vline.into_value(),
- "stroke" => self.stroke.into_value()
- };
-
- d.into_value()
- },
- v: isize => Augment {
- hline: Offsets::default(),
- vline: Offsets(smallvec![v]),
- stroke: Smart::Auto,
- },
- mut dict: Dict => {
- // need the transpose for the defaults to work
- let hline = dict.take("hline").ok().map(Offsets::from_value)
- .transpose().unwrap_or_default().unwrap_or_default();
- let vline = dict.take("vline").ok().map(Offsets::from_value)
- .transpose().unwrap_or_default().unwrap_or_default();
-
- let stroke = dict.take("stroke").ok().map(Stroke::from_value)
- .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto);
-
- Augment { hline, vline, stroke }
- },
-}
-
-cast! {
- Augment<Abs>,
- self => self.into_value(),
-}
-
-/// The offsets at which augmentation lines should be drawn on a matrix.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct Offsets(SmallVec<[isize; 1]>);
-
-cast! {
- Offsets,
- self => self.0.into_value(),
- v: isize => Self(smallvec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs
deleted file mode 100644
index 7ced638b..00000000
--- a/crates/typst-library/src/math/mod.rs
+++ /dev/null
@@ -1,500 +0,0 @@
-//! Mathematical formulas.
-
-#[macro_use]
-mod ctx;
-mod accent;
-mod align;
-mod attach;
-mod cancel;
-mod class;
-mod frac;
-mod fragment;
-mod lr;
-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::class::*;
-pub use self::frac::*;
-pub use self::lr::*;
-pub use self::matrix::*;
-pub use self::op::*;
-pub use self::root::*;
-pub use self::style::*;
-pub use self::underover::*;
-
-use std::borrow::Cow;
-
-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::{AlignElem, BoxElem, HElem, ParElem, Spacing};
-use crate::meta::{
- Count, Counter, CounterUpdate, LocalNameIn, Numbering, Outlinable, Refable,
- Supplement,
-};
-use crate::prelude::*;
-use crate::shared::BehavedBuilder;
-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.category("math");
- math.define_elem::<EquationElem>();
- math.define_elem::<TextElem>();
- math.define_elem::<LrElem>();
- math.define_elem::<AttachElem>();
- math.define_elem::<ScriptsElem>();
- math.define_elem::<LimitsElem>();
- math.define_elem::<AccentElem>();
- math.define_elem::<UnderlineElem>();
- math.define_elem::<OverlineElem>();
- math.define_elem::<UnderbraceElem>();
- math.define_elem::<OverbraceElem>();
- math.define_elem::<UnderbracketElem>();
- math.define_elem::<OverbracketElem>();
- math.define_elem::<CancelElem>();
- math.define_elem::<FracElem>();
- math.define_elem::<BinomElem>();
- math.define_elem::<VecElem>();
- math.define_elem::<MatElem>();
- math.define_elem::<CasesElem>();
- math.define_elem::<RootElem>();
- math.define_elem::<ClassElem>();
- math.define_elem::<OpElem>();
- math.define_func::<abs>();
- math.define_func::<norm>();
- math.define_func::<floor>();
- math.define_func::<ceil>();
- math.define_func::<round>();
- math.define_func::<sqrt>();
- math.define_func::<upright>();
- math.define_func::<bold>();
- math.define_func::<italic>();
- math.define_func::<serif>();
- math.define_func::<sans>();
- math.define_func::<cal>();
- math.define_func::<frak>();
- math.define_func::<mono>();
- math.define_func::<bb>();
- math.define_func::<display>();
- math.define_func::<inline>();
- math.define_func::<script>();
- math.define_func::<sscript>();
-
- // Text operators, spacings, and symbols.
- op::define(&mut math);
- spacing::define(&mut math);
- for (name, symbol) in crate::symbols::SYM {
- math.define(*name, symbol.clone());
- }
-
- Module::new("math", math)
-}
-
-/// A mathematical equation.
-///
-/// Can be displayed inline with text or as a separate block.
-///
-/// # Example
-/// ```example
-/// #set text(font: "New Computer Modern")
-///
-/// Let $a$, $b$, and $c$ be the side
-/// lengths of right-angled triangle.
-/// Then, we know that:
-/// $ a^2 + b^2 = c^2 $
-///
-/// Prove by induction:
-/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: Write mathematical markup within
-/// dollar signs to create an equation. Starting and ending the equation with at
-/// least one space lifts it into a separate block that is centered
-/// horizontally. For more details about math syntax, see the
-/// [main math page]($category/math).
-#[elem(
- Locatable, Synthesize, Show, 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]($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::elem()));
- if self.block(styles) {
- realized = AlignElem::new(realized).pack();
- }
- Ok(realized)
- }
-}
-
-impl Finalize for EquationElem {
- fn finalize(&self, realized: Content, style: StyleChain) -> Content {
- let mut realized = realized;
- if self.block(style) {
- realized = realized.styled(AlignElem::set_alignment(Align::CENTER));
- }
- 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, 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::elem())
- .display(Some(numbering), false)
- .layout(vt, styles, pod)?
- .into_frame();
-
- let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles);
- let width = if regions.size.x.is_finite() {
- regions.size.x
- } else {
- frame.width() + 2.0 * full_counter_width
- };
-
- let height = frame.height().max(counter.height());
- let align = AlignElem::alignment_in(styles).resolve(styles).x;
- frame.resize(Size::new(width, height), Axes::splat(align));
-
- let dir = TextElem::dir_in(styles);
- let offset = match (align, dir) {
- (FixedAlign::Start, Dir::RTL) => full_counter_width,
- (FixedAlign::End, Dir::LTR) => -full_counter_width,
- _ => Abs::zero(),
- };
- frame.translate(Point::with_x(offset));
-
- let x = if dir.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 font_size = TextElem::size_in(styles);
- let slack = ParElem::leading_in(styles) * 0.7;
- let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
- let bottom_edge =
- -TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
-
- 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(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::FINNISH => "Yhtälö",
- Lang::FRENCH => "Équation",
- Lang::GERMAN => "Gleichung",
- Lang::GREEK => "Εξίσωση",
- Lang::HUNGARIAN => "Egyenlet",
- Lang::ITALIAN => "Equazione",
- Lang::NYNORSK => "Likning",
- Lang::POLISH => "Równanie",
- Lang::PORTUGUESE => "Equação",
- Lang::ROMANIAN => "Ecuația",
- 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::JAPANESE => "式",
- 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::elem())
- }
-
- fn numbering(&self) -> Option<Numbering> {
- self.numbering(StyleChain::default())
- }
-}
-
-impl Outlinable for EquationElem {
- fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
- if !self.block(StyleChain::default()) {
- return Ok(None);
- }
- 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.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 self.is_sequence() {
- let mut bb = BehavedBuilder::new();
- self.sequence_recursive_for_each(&mut |child: &Content| {
- bb.push(Cow::Owned(child.clone()), StyleChain::default())
- });
-
- for (child, _) in bb.finish().0.iter() {
- 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(boxed) = self.to::<BoxElem>() {
- let frame = ctx.layout_box(boxed)?;
- ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
- 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
deleted file mode 100644
index 9e35d207..00000000
--- a/crates/typst-library/src/math/op.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use super::*;
-
-/// A text operator in an equation.
-///
-/// # Example
-/// ```example
-/// $ tan x = (sin x)/(cos x) $
-/// $ op("custom",
-/// limits: #true)_(n->oo) n $
-/// ```
-///
-/// # Predefined Operators { #predefined }
-/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
-/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
-/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
-/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
-/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
-#[elem(title = "Text Operator", LayoutMath)]
-pub struct OpElem {
- /// The operator's text.
- #[required]
- pub text: Content,
-
- /// Whether the operator should show attachments as limits in display mode.
- #[default(false)]
- pub limits: bool,
-}
-
-impl LayoutMath for OpElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let fragment = ctx.layout_fragment(self.text())?;
- 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) {
- $({
- let operator = EcoString::from(ops!(@name $name $(: $value)?));
- math.define(
- stringify!($name),
- OpElem::new(TextElem::new(operator).into())
- .with_limits(ops!(@limit $($tts)*))
- .pack()
- );
- })*
-
- let dif = |d| {
- HElem::new(THIN.into()).with_weak(true).pack()
- + 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,
- coth,
- csc,
- csch,
- ctg,
- deg,
- det (limits),
- dim,
- exp,
- gcd (limits),
- hom,
- id,
- im,
- inf (limits),
- ker,
- lg,
- lim (limits),
- liminf: "lim inf" (limits),
- limsup: "lim sup" (limits),
- ln,
- log,
- max (limits),
- min (limits),
- mod,
- Pr (limits),
- sec,
- sech,
- sin,
- sinc,
- sinh,
- sup (limits),
- tan,
- tanh,
- tg,
- tr,
-}
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
deleted file mode 100644
index ba918ea9..00000000
--- a/crates/typst-library/src/math/root.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-use super::*;
-
-/// A square root.
-///
-/// ```example
-/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
-/// ```
-#[func(title = "Square Root")]
-pub fn sqrt(
- /// The expression to take the square root of.
- radicand: Content,
-) -> Content {
- RootElem::new(radicand).pack()
-}
-
-/// A general root.
-///
-/// ```example
-/// $ root(3, x) $
-/// ```
-#[elem(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.
-///
-/// TeXbook page 443, page 360
-/// See also: https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
-fn layout(
- ctx: &mut MathContext,
- 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 = GlyphFragment::new(ctx, '√', span)
- .stretch_vertical(ctx, target, Abs::zero())
- .frame;
-
- // Layout the index.
- ctx.style(ctx.style.with_size(MathSize::ScriptScript));
- let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?;
- ctx.unstyle();
-
- // TeXbook, page 443, item 11
- // Keep original gap, and then distribute any remaining free space
- // equally above and below.
- let gap = gap.max((sqrt.height() - thickness - radicand.height() + gap) / 2.0);
-
- let sqrt_ascent = radicand.ascent() + gap + thickness;
- let descent = sqrt.height() - sqrt_ascent;
- let inner_ascent = sqrt_ascent + extra_ascender;
-
- 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;
- // The formula below for how much raise the index by comes from
- // the TeXbook, page 360, in the definition of `\root`.
- // However, the `+ index.descent()` part is different from TeX.
- // Without it, descenders can collide with the surd, a rarity
- // in practice, but possible. MS Word also adjusts index positions
- // for descenders.
- shift_up = raise_factor * (inner_ascent - descent) + index.descent();
- ascent.set_max(shift_up + index.ascent());
- }
-
- let radicand_x = sqrt_offset + sqrt.width();
- let radicand_y = ascent - radicand.ascent();
- let width = radicand_x + radicand.width();
- let size = Size::new(width, ascent + descent);
-
- // The extra "- thickness" comes from the fact that the sqrt is placed
- // in `push_frame` with respect to its top, not its baseline.
- let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness);
- let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
- let radicand_pos = Point::new(radicand_x, radicand_y);
-
- let mut frame = Frame::soft(size);
- frame.set_baseline(ascent);
-
- if let Some(index) = index {
- let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up);
- 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(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness,
- ..FixedStroke::default()
- }),
- span,
- ),
- );
-
- frame.push_frame(radicand_pos, radicand);
- ctx.push(FrameFragment::new(ctx, frame));
-
- Ok(())
-}
diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs
deleted file mode 100644
index 70813598..00000000
--- a/crates/typst-library/src/math/row.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-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).resolve(styles).x;
- 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: FixedAlign,
- ) -> Frame {
- if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
- return self.into_line_frame(points, align);
- }
-
- 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::soft(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
- }
-
- fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame {
- let ascent = self.ascent();
- let mut frame = Frame::soft(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 != FixedAlign::Start {
- 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 {
- FixedAlign::Start => prev_points.next(),
- FixedAlign::End => {
- 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
deleted file mode 100644
index 3dfce024..00000000
--- a/crates/typst-library/src/math/spacing.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-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);
-pub(super) const WIDE: Em = Em::new(2.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());
- math.define("wide", HElem::new(WIDE.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 to the left of
- // an opening delimiter. TeXBook, p170
- (Large, Opening | Fence) => 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
deleted file mode 100644
index e9bf6890..00000000
--- a/crates/typst-library/src/math/stretch.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-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::soft(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
deleted file mode 100644
index 774fadac..00000000
--- a/crates/typst-library/src/math/style.rs
+++ /dev/null
@@ -1,574 +0,0 @@
-use super::*;
-
-/// Bold font style in math.
-///
-/// ```example
-/// $ bold(A) := B^+ $
-/// ```
-#[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
-/// $ upright(A) != A $
-/// ```
-#[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.
-#[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.
-#[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
-/// $ sans(A B C) $
-/// ```
-#[func(title = "Sans Serif")]
-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
-/// Let $cal(P)$ be the set of ...
-/// ```
-#[func(title = "Calligraphic")]
-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
-/// $ frak(P) $
-/// ```
-#[func(title = "Fraktur")]
-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
-/// $ mono(x + y = z) $
-/// ```
-#[func(title = "Monospace")]
-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
-/// $ bb(b) $
-/// $ bb(N) = NN $
-/// $ f: NN -> RR $
-/// ```
-#[func(title = "Blackboard Bold")]
-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
-/// $sum_i x_i/2 = display(sum_i x_i/2)$
-/// ```
-#[func(title = "Display Size")]
-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
-/// $ sum_i x_i/2
-/// = inline(sum_i x_i/2) $
-/// ```
-#[func(title = "Inline Size")]
-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
-/// $sum_i x_i/2 = script(sum_i x_i/2)$
-/// ```
-#[func(title = "Script Size")]
-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
-/// $sum_i x_i/2 = sscript(sum_i x_i/2)$
-/// ```
-#[func(title = "Script-Script Size")]
-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.
-#[elem(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,
- /// The class of the element.
- pub class: Smart<MathClass>,
- /// 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 the given `class`.
- pub fn with_class(self, class: MathClass) -> Self {
- Self { class: Smart::Custom(class), ..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, Hash)]
-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, Hash)]
-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
deleted file mode 100644
index 6fc76830..00000000
--- a/crates/typst-library/src/math/underover.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-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
-/// $ underline(1 + 2 + ... + 5) $
-/// ```
-#[elem(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
-/// $ overline(1 + 2 + ... + 5) $
-/// ```
-#[elem(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::soft(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(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness: bar_height,
- ..FixedStroke::default()
- }),
- span,
- ),
- );
-
- ctx.push(FrameFragment::new(ctx, frame).with_class(content_class));
-
- Ok(())
-}
-
-/// A horizontal brace under content, with an optional annotation below.
-///
-/// ```example
-/// $ underbrace(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(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
-/// $ overbrace(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(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
-/// $ underbracket(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(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
-/// $ overbracket(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(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, FixedAlign::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: FixedAlign,
- 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::soft(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
-}