Skip to main content

typst_library/math/ir/
item.rs

1#![allow(clippy::too_many_arguments)]
2use std::cell::Cell;
3use std::ops::{Deref, MulAssign};
4use std::rc::Rc;
5
6use ecow::EcoString;
7use typst_syntax::Span;
8use typst_utils::{Get, default_math_class};
9use unicode_math_class::MathClass;
10use unicode_segmentation::UnicodeSegmentation;
11
12use super::multiline::AlignedRow;
13use crate::diag::SourceResult;
14use crate::foundations::{Content, Packed, Smart, StyleChain};
15use crate::introspection::{Locator, Tag};
16use crate::layout::{
17    Abs, Axes, Axis, BoxElem, Em, FixedAlignment, Length, PlaceElem, Ratio, Rel,
18};
19use crate::math::{
20    Augment, CancelAngle, EquationElem, LeftRightAlternator, Limits, MathSize,
21};
22use crate::visualize::FixedStroke;
23
24/// An element in the resolver's item stream: either a math item, or a
25/// `Linebreak` or `Align` item that only exist during resolution.
26#[derive(Debug)]
27pub(crate) enum RawMathItem<'a> {
28    /// A math item.
29    Item(MathItem<'a>),
30    /// A line break.
31    Linebreak,
32    /// An alignment point.
33    Align,
34}
35
36impl<'a> From<MathItem<'a>> for RawMathItem<'a> {
37    fn from(item: MathItem<'a>) -> Self {
38        Self::Item(item)
39    }
40}
41
42impl<'a> RawMathItem<'a> {
43    /// Whether this item should be ignored for spacing calculations.
44    pub(crate) fn is_ignorant(&self) -> bool {
45        match self {
46            Self::Item(item) => item.is_ignorant(),
47            Self::Linebreak | Self::Align => false,
48        }
49    }
50
51    /// Unwraps this into its inner [`MathItem`].
52    ///
53    /// Returns `None` if this is a [`Linebreak`](RawMathItem::Linebreak) or
54    /// [`Align`](RawMathItem::Align).
55    pub(crate) fn into_item(self) -> Option<MathItem<'a>> {
56        match self {
57            Self::Item(item) => Some(item),
58            Self::Linebreak | Self::Align => None,
59        }
60    }
61}
62
63/// The top-level item in the math IR.
64#[derive(Debug)]
65pub enum MathItem<'a> {
66    /// A layoutable component with associated properties and styles.
67    Component(MathComponent<'a>),
68    /// Explicit spacing with the font size at the point of creation. The
69    /// boolean indicates whether the spacing is weak.
70    Spacing(Length, Abs, bool),
71    /// A regular space.
72    Space,
73    /// An introspection tag.
74    Tag(Tag),
75}
76
77impl<'a> From<MathComponent<'a>> for MathItem<'a> {
78    fn from(comp: MathComponent<'a>) -> Self {
79        Self::Component(comp)
80    }
81}
82
83impl<'a> MathItem<'a> {
84    /// Wraps the given items into a group item, or returns the single item if
85    /// there is only one.
86    pub(crate) fn wrap(
87        mut items: Vec<MathItem<'a>>,
88        styles: StyleChain<'a>,
89    ) -> MathItem<'a> {
90        if items.len() == 1 {
91            items.pop().unwrap()
92        } else {
93            GroupItem::create(items, styles)
94        }
95    }
96
97    /// Returns the limit placement configuration for this item.
98    pub(crate) fn limits(&self) -> Limits {
99        match self {
100            Self::Component(comp) => comp.props.limits,
101            _ => Limits::Never,
102        }
103    }
104
105    /// Returns the math class of this item.
106    pub(crate) fn class(&self) -> MathClass {
107        self.raw_class().unwrap_or(MathClass::Normal)
108    }
109
110    pub(crate) fn raw_class(&self) -> Option<MathClass> {
111        match self {
112            Self::Component(comp) => comp.props.class,
113            Self::Spacing(..) | Self::Space => Some(MathClass::Space),
114            Self::Tag(_) => Some(MathClass::Special),
115        }
116    }
117
118    /// Returns the effective math class on the right side of this item.
119    ///
120    /// For fenced items with a closing delimiter and no explicit class, this
121    /// returns the closing class instead of the item's overall class.
122    pub(crate) fn rclass(&self) -> MathClass {
123        match self {
124            Self::Component(MathComponent {
125                kind: MathKind::Fenced(fence),
126                props: MathProperties { class: None, .. },
127                ..
128            }) if fence.close.is_some() => MathClass::Closing,
129            _ => self.class(),
130        }
131    }
132
133    /// Returns the effective math class on the left side of this item.
134    ///
135    /// For fenced items with an opening delimiter and no explicit class, this
136    /// returns the opening class instead of the item's overall class.
137    pub(crate) fn lclass(&self) -> MathClass {
138        match self {
139            Self::Component(MathComponent {
140                kind: MathKind::Fenced(fence),
141                props: MathProperties { class: None, .. },
142                ..
143            }) if fence.open.is_some() => MathClass::Opening,
144            _ => self.class(),
145        }
146    }
147
148    /// Returns the math size of this item, if it is a component.
149    pub(crate) fn size(&self) -> Option<MathSize> {
150        match self {
151            Self::Component(comp) => Some(comp.props.size),
152            _ => None,
153        }
154    }
155
156    /// Whether this item should have explicit spaces around it.
157    pub(crate) fn is_spaced(&self) -> bool {
158        if self.class() == MathClass::Fence {
159            return true;
160        }
161
162        if let Self::Component(comp) = self
163            && comp.props.spaced
164            && matches!(comp.props.class(), MathClass::Normal | MathClass::Alphabetic)
165        {
166            true
167        } else {
168            false
169        }
170    }
171
172    /// Whether this item should be ignored for spacing calculations.
173    pub fn is_ignorant(&self) -> bool {
174        match self {
175            Self::Component(comp) => comp.props.ignorant,
176            Self::Tag(_) => true,
177            _ => false,
178        }
179    }
180
181    /// Returns the source span of this item.
182    pub fn span(&self) -> Span {
183        match self {
184            Self::Component(comp) => comp.props.span,
185            _ => Span::detached(),
186        }
187    }
188
189    /// Returns the style chain of this item, if it is a component.
190    pub fn styles(&self) -> Option<StyleChain<'a>> {
191        match self {
192            Self::Component(comp) => Some(comp.styles),
193            _ => None,
194        }
195    }
196
197    /// Returns whether this glyph has been stretched as a middle delimiter.
198    pub fn mid_stretched(&self) -> Option<bool> {
199        if let Self::Component(comp) = self
200            && let MathKind::Glyph(glyph) = &comp.kind
201        {
202            glyph.mid_stretched.get()
203        } else {
204            None
205        }
206    }
207
208    /// Whether this item is a multiline item.
209    pub fn is_multiline(&self) -> bool {
210        matches!(
211            self,
212            MathItem::Component(MathComponent { kind: MathKind::Multiline(_), .. })
213        )
214    }
215
216    /// Returns the inner items if this is a group, or a slice containing
217    /// just this item otherwise.
218    pub fn as_slice(&self) -> &[MathItem<'a>] {
219        if let MathItem::Component(comp) = self
220            && let MathKind::Group(group) = &comp.kind
221        {
222            &group.items
223        } else {
224            core::slice::from_ref(self)
225        }
226    }
227
228    /// Sets the limit placement configuration for this item.
229    pub(crate) fn set_limits(&mut self, limits: Limits) {
230        if let Self::Component(comp) = self {
231            comp.props.limits = limits;
232        }
233    }
234
235    /// Sets the effective math class of this item.
236    pub(crate) fn set_class(&mut self, class: MathClass) {
237        if let Self::Component(comp) = self {
238            comp.props.class = Some(class);
239        }
240    }
241
242    /// Sets the effective math class and applies it to glyph layout.
243    pub(crate) fn set_explicit_class(&mut self, class: MathClass) {
244        self.set_class(class);
245        if let Self::Component(comp) = self
246            && let MathKind::Glyph(glyph) = &mut comp.kind
247        {
248            glyph.class = class;
249
250            // Small hack to ensure the non-explicit stretch gets added, as the
251            // class is not recursive. This applies an equivalent stretch to
252            // the one in `resolve_symbol`.
253            if class == MathClass::Large
254                && comp.props.size == MathSize::Display
255                && !glyph.stretch.get().is_explicit(Axis::Y)
256            {
257                let info = StretchInfo::default();
258                glyph.stretch.update(|stretch| stretch.with_y(info));
259            }
260        }
261    }
262
263    /// Sets the left spacing for this item if not already set.
264    pub(crate) fn set_lspace(&mut self, lspace: Option<Em>) {
265        if let Self::Component(comp) = self
266            && comp.props.lspace.is_none()
267        {
268            comp.props.lspace = lspace;
269        }
270    }
271
272    /// Sets the right spacing for this item if not already set.
273    pub(crate) fn set_rspace(&mut self, rspace: Option<Em>) {
274        if let Self::Component(comp) = self
275            && comp.props.rspace.is_none()
276        {
277            comp.props.rspace = rspace;
278        }
279    }
280
281    /// If this is a multiline item, sets the centered field to true.
282    pub(crate) fn with_multiline_centering(mut self) -> Self {
283        if let Self::Component(comp) = &mut self
284            && let MathKind::Multiline(multiline) = &mut comp.kind
285        {
286            multiline.centered = true;
287        }
288        self
289    }
290
291    /// Sets whether this glyph has been stretched as a middle delimiter.
292    pub(crate) fn set_mid_stretched(&self, mid_stretched: Option<bool>) {
293        if let Self::Component(comp) = self
294            && let MathKind::Glyph(glyph) = &comp.kind
295        {
296            glyph.mid_stretched.set(mid_stretched);
297        }
298    }
299
300    /// Sets the stretch configuration for this glyph, marking it as explicit.
301    pub(crate) fn set_stretch(&self, mut stretch: Stretch) {
302        if let Some(info) = &mut stretch.0.x {
303            info.explicit = true;
304        }
305        if let Some(info) = &mut stretch.0.y {
306            info.explicit = true;
307        }
308        self.replace_stretch(stretch);
309    }
310
311    /// Sets the stretch configuration for this glyph
312    pub(crate) fn replace_stretch(&self, stretch: Stretch) {
313        if let Self::Component(comp) = self
314            && let MathKind::Glyph(glyph) = &comp.kind
315        {
316            glyph.stretch.replace(stretch);
317        }
318    }
319
320    /// Updates the vertical stretch info for this glyph.
321    pub(crate) fn set_y_stretch(&self, mut info: StretchInfo) {
322        if let Self::Component(comp) = self
323            && let MathKind::Glyph(glyph) = &comp.kind
324        {
325            info.explicit = true;
326            glyph.stretch.update(|stretch| stretch.with_y(info));
327        }
328    }
329
330    /// Updates the stretch info for both axes of this glyph.
331    pub(crate) fn update_stretch(&self, info: StretchInfo) {
332        if let Self::Component(comp) = self
333            && let MathKind::Glyph(glyph) = &comp.kind
334        {
335            glyph.stretch.update(|stretch| stretch.update(info));
336        }
337    }
338
339    /// Sets the reference size for relative stretching on the given axis.
340    pub fn set_stretch_relative_to(&self, relative_to: Abs, axis: Axis) {
341        if let Self::Component(comp) = self
342            && let MathKind::Glyph(glyph) = &comp.kind
343        {
344            glyph.stretch.update(|stretch| stretch.relative_to(relative_to, axis));
345        }
346    }
347
348    /// Sets the font size to use for short-fall calculations on the given axis.
349    pub fn set_stretch_font_size(&self, font_size: Abs, axis: Axis) {
350        if let Self::Component(comp) = self
351            && let MathKind::Glyph(glyph) = &comp.kind
352        {
353            glyph.stretch.update(|stretch| stretch.font_size(font_size, axis));
354        }
355    }
356
357    /// Enables the flac OpenType feature for this glyph.
358    pub fn set_flac(&self) {
359        if let Self::Component(comp) = self
360            && let MathKind::Glyph(glyph) = &comp.kind
361        {
362            glyph.flac.set(true);
363        }
364    }
365}
366
367/// A generic component that bundles a specific math item kind with common
368/// properties and styles.
369#[derive(Debug)]
370pub struct MathComponent<'a> {
371    /// The specific kind of math item.
372    pub kind: MathKind<'a>,
373    /// The properties attached to this component.
374    pub props: MathProperties,
375    /// The item's styles.
376    pub styles: StyleChain<'a>,
377}
378
379/// The specific kind of a layoutable math item.
380///
381/// Recursive or large variants are boxed.
382#[derive(Debug)]
383pub enum MathKind<'a> {
384    /// A group of math items laid out horizontally.
385    Group(GroupItem<'a>),
386    /// A multiline equation with items pre-split into rows and columns.
387    Multiline(MultilineItem<'a>),
388    /// A radical (square root or nth root).
389    Radical(Box<RadicalItem<'a>>),
390    /// An item enclosed in delimiters.
391    Fenced(Box<FencedItem<'a>>),
392    /// A vertical fraction.
393    Fraction(Box<FractionItem<'a>>),
394    /// An inline skewed fraction.
395    SkewedFraction(Box<SkewedFractionItem<'a>>),
396    /// A 2D collection of math items laid out as a table/matrix.
397    Table(Box<TableItem<'a>>),
398    /// A base with scripts (subscripts/superscripts) and/or limits attached.
399    Scripts(Box<ScriptsItem<'a>>),
400    /// A base with an accent mark above or below.
401    Accent(Box<AccentItem<'a>>),
402    /// A base with a line overlaid.
403    Cancel(Box<CancelItem<'a>>),
404    /// A base with a line drawn above or below.
405    Line(Box<LineItem<'a>>),
406    /// Grouped prime symbols.
407    Primes(Box<PrimesItem>),
408    /// A text string.
409    Text(TextItem<'a>),
410    /// A number.
411    Number(NumberItem),
412    /// A single glyph (grapheme cluster).
413    Glyph(Box<GlyphItem>),
414    /// Inline content.
415    Box(BoxItem<'a>),
416    /// A MathML HTML element.
417    Mathml(Box<MathmlItem<'a>>),
418    /// External content that needs to be laid out separately.
419    External(ExternalItem<'a>),
420}
421
422/// Shared properties for all layoutable math components.
423#[derive(Debug, Copy, Clone)]
424pub struct MathProperties {
425    /// How attachments should be positioned.
426    pub(crate) limits: Limits,
427    /// The math class.
428    pub class: Option<MathClass>,
429    /// The current math size.
430    pub size: MathSize,
431    /// Whether this item is in a cramped style.
432    pub cramped: bool,
433    /// Whether this item should be ignored for spacing calculations.
434    pub(crate) ignorant: bool,
435    /// Whether this item should have explicit spaces around it.
436    pub(crate) spaced: bool,
437    /// The amount of spacing to the left of this item.
438    pub lspace: Option<Em>,
439    /// The amount of spacing to the right of this item.
440    pub rspace: Option<Em>,
441    /// Whether this item is at the start of a left-aligned column but
442    /// semantically infix.
443    pub align_form_infix: bool,
444    /// The source span.
445    pub span: Span,
446}
447
448impl MathProperties {
449    /// Creates properties with an explicit class, avoiding the style lookup.
450    fn new(styles: StyleChain, class: Option<MathClass>, span: Span) -> MathProperties {
451        Self {
452            limits: Limits::Never,
453            class,
454            size: styles.get(EquationElem::size),
455            cramped: styles.get(EquationElem::cramped),
456            ignorant: false,
457            spaced: false,
458            lspace: None,
459            rspace: None,
460            align_form_infix: false,
461            span,
462        }
463    }
464
465    /// Creates default properties from the given styles.
466    ///
467    /// This gets the math size from the styles.
468    pub fn default(styles: StyleChain, span: Span) -> MathProperties {
469        Self::new(styles, None, span)
470    }
471
472    /// Returns the class, using the default normal class if None.
473    pub fn class(&self) -> MathClass {
474        self.class.unwrap_or(MathClass::Normal)
475    }
476
477    /// Sets how attachments should be positioned for this item.
478    fn with_limits(mut self, limits: Limits) -> Self {
479        self.limits = limits;
480        self
481    }
482
483    /// Sets whether this item should be ignored for spacing calculations.
484    fn with_ignorant(mut self, ignorant: bool) -> Self {
485        self.ignorant = ignorant;
486        self
487    }
488
489    /// Sets whether this item should have explicit spaces around it.
490    fn with_spaced(mut self, spaced: bool) -> Self {
491        self.spaced = spaced;
492        self
493    }
494}
495
496/// A group of math items laid out horizontally.
497#[derive(Debug)]
498pub struct GroupItem<'a> {
499    /// The items in the group.
500    pub items: Vec<MathItem<'a>>,
501}
502
503impl<'a> GroupItem<'a> {
504    /// Creates a new group item.
505    pub(crate) fn create(
506        items: Vec<MathItem<'a>>,
507        styles: StyleChain<'a>,
508    ) -> MathItem<'a> {
509        let props = MathProperties::default(styles, Span::detached());
510        let kind = MathKind::Group(Self { items });
511        MathComponent { kind, props, styles }.into()
512    }
513}
514
515/// A multiline equation with items pre-split into rows and columns.
516#[derive(Debug)]
517pub struct MultilineItem<'a> {
518    /// The cells, organized by row.
519    ///
520    /// Rows correspond to linebreaks in the source. Columns within each row
521    /// correspond to alignment points. All rows are padded to have the same
522    /// number of columns.
523    pub rows: Vec<AlignedRow<'a>>,
524    /// Whether the resulting frame should be aligned on the math axis.
525    ///
526    /// Only used in paged export.
527    pub centered: bool,
528}
529
530impl<'a> MultilineItem<'a> {
531    /// Creates a new multiline item.
532    pub(crate) fn create(
533        rows: Vec<AlignedRow<'a>>,
534        styles: StyleChain<'a>,
535    ) -> MathItem<'a> {
536        let kind = MathKind::Multiline(Self { rows, centered: false });
537        let props = MathProperties::default(styles, Span::detached());
538        MathComponent { kind, props, styles }.into()
539    }
540}
541
542/// A radical (square root or nth root).
543#[derive(Debug)]
544pub struct RadicalItem<'a> {
545    /// The item under the radical symbol.
546    pub radicand: MathItem<'a>,
547    /// The index for nth roots. `None` for square roots.
548    pub index: Option<MathItem<'a>>,
549    /// The radical symbol.
550    ///
551    /// Only used in paged export.
552    pub sqrt: MathItem<'a>,
553}
554
555impl<'a> RadicalItem<'a> {
556    /// Creates a new radical item.
557    pub(crate) fn create(
558        radicand: MathItem<'a>,
559        index: Option<MathItem<'a>>,
560        sqrt: MathItem<'a>,
561        styles: StyleChain<'a>,
562        span: Span,
563    ) -> MathItem<'a> {
564        let kind = MathKind::Radical(Box::new(Self { radicand, index, sqrt }));
565        let props = MathProperties::default(styles, span);
566        MathComponent { kind, props, styles }.into()
567    }
568}
569
570/// An item enclosed in delimiters.
571#[derive(Debug)]
572pub struct FencedItem<'a> {
573    /// The optional opening delimiter.
574    pub open: Option<MathItem<'a>>,
575    /// The optional closing delimiter.
576    pub close: Option<MathItem<'a>>,
577    /// The item between the delimiters.
578    pub body: FencedBody<'a>,
579    /// How the target height for the delimiters should be calculated.
580    ///
581    /// If true, the height for each body item is two times the maximum of its
582    /// ascent and descent. If false, the height for each body item is simply
583    /// its height.
584    ///
585    /// Only used in paged export.
586    pub balanced: bool,
587}
588
589impl<'a> FencedItem<'a> {
590    /// Creates a new fenced item.
591    pub(crate) fn create(
592        open: Option<MathItem<'a>>,
593        close: Option<MathItem<'a>>,
594        body: impl Into<FencedBody<'a>>,
595        balanced: bool,
596        styles: StyleChain<'a>,
597        span: Span,
598    ) -> MathItem<'a> {
599        let kind =
600            MathKind::Fenced(Box::new(Self { open, close, body: body.into(), balanced }));
601        let props = MathProperties::default(styles, span);
602        MathComponent { kind, props, styles }.into()
603    }
604}
605
606/// A vertical fraction.
607#[derive(Debug)]
608pub struct FractionItem<'a> {
609    /// The item in the top part of the fraction.
610    pub numerator: MathItem<'a>,
611    /// The item in the bottom part of the fraction.
612    pub denominator: MathItem<'a>,
613    /// Whether to draw a fraction line between the numerator and denominator.
614    pub line: bool,
615    /// The amount of padding added before and after the fraction.
616    pub padding: Em,
617}
618
619impl<'a> FractionItem<'a> {
620    /// Creates a new fraction item.
621    pub(crate) fn create(
622        numerator: MathItem<'a>,
623        denominator: MathItem<'a>,
624        line: bool,
625        padding: Em,
626        styles: StyleChain<'a>,
627        span: Span,
628    ) -> MathItem<'a> {
629        let kind =
630            MathKind::Fraction(Box::new(Self { numerator, denominator, line, padding }));
631        let props = MathProperties::default(styles, span);
632        MathComponent { kind, props, styles }.into()
633    }
634}
635
636/// An inline skewed fraction.
637#[derive(Debug)]
638pub struct SkewedFractionItem<'a> {
639    /// The item in the top-left part of the fraction.
640    pub numerator: MathItem<'a>,
641    /// The item in the bottom-right part of the fraction.
642    pub denominator: MathItem<'a>,
643    /// The fraction slash symbol.
644    ///
645    /// Only used in paged export.
646    pub slash: MathItem<'a>,
647}
648
649impl<'a> SkewedFractionItem<'a> {
650    /// Creates a new skewed fraction item.
651    pub(crate) fn create(
652        numerator: MathItem<'a>,
653        denominator: MathItem<'a>,
654        slash: MathItem<'a>,
655        styles: StyleChain<'a>,
656        span: Span,
657    ) -> MathItem<'a> {
658        let kind =
659            MathKind::SkewedFraction(Box::new(Self { numerator, denominator, slash }));
660        let props = MathProperties::default(styles, span);
661        MathComponent { kind, props, styles }.into()
662    }
663}
664
665/// A 2D collection of math items laid out as a table/matrix.
666#[derive(Debug)]
667pub struct TableItem<'a> {
668    /// The cells of the table, organized by row.
669    pub cells: Vec<Vec<AlignedRow<'a>>>,
670    /// The gap between rows and columns.
671    pub gap: Axes<Rel<Abs>>,
672    /// Optional augmentation lines to draw.
673    pub augment: Option<Augment<Abs>>,
674    /// The alignment for cells.
675    pub align: FixedAlignment,
676    /// How to perform left/right alternation for alignment.
677    pub alternator: LeftRightAlternator,
678}
679
680impl<'a> TableItem<'a> {
681    /// Creates a new table item.
682    pub(crate) fn create(
683        cells: Vec<Vec<AlignedRow<'a>>>,
684        gap: Axes<Rel<Abs>>,
685        augment: Option<Augment<Abs>>,
686        align: FixedAlignment,
687        alternator: LeftRightAlternator,
688        styles: StyleChain<'a>,
689        span: Span,
690    ) -> MathItem<'a> {
691        let kind =
692            MathKind::Table(Box::new(Self { cells, gap, augment, align, alternator }));
693        let props = MathProperties::default(styles, span);
694        MathComponent { kind, props, styles }.into()
695    }
696}
697
698/// A base with scripts (subscripts/superscripts) and/or limits attached.
699#[derive(Debug)]
700pub struct ScriptsItem<'a> {
701    /// The base item.
702    pub base: MathItem<'a>,
703    /// The top attachment (limit above).
704    pub top: Option<MathItem<'a>>,
705    /// The bottom attachment (limit below).
706    pub bottom: Option<MathItem<'a>>,
707    /// The top-left attachment (pre-superscript).
708    pub top_left: Option<MathItem<'a>>,
709    /// The bottom-left attachment (pre-subscript).
710    pub bottom_left: Option<MathItem<'a>>,
711    /// The top-right attachment (post-superscript).
712    pub top_right: Option<MathItem<'a>>,
713    /// The bottom-right attachment (post-subscript).
714    pub bottom_right: Option<MathItem<'a>>,
715}
716
717impl<'a> ScriptsItem<'a> {
718    /// Creates a new scripts item.
719    ///
720    /// The resulting item inherits its math class from the base.
721    pub(crate) fn create(
722        base: MathItem<'a>,
723        top: Option<MathItem<'a>>,
724        bottom: Option<MathItem<'a>>,
725        top_left: Option<MathItem<'a>>,
726        bottom_left: Option<MathItem<'a>>,
727        top_right: Option<MathItem<'a>>,
728        bottom_right: Option<MathItem<'a>>,
729        styles: StyleChain<'a>,
730    ) -> MathItem<'a> {
731        let props = MathProperties::new(styles, base.raw_class(), Span::detached());
732        let kind = MathKind::Scripts(Box::new(Self {
733            base,
734            top,
735            bottom,
736            top_left,
737            bottom_left,
738            top_right,
739            bottom_right,
740        }));
741        MathComponent { kind, props, styles }.into()
742    }
743}
744
745/// A base with an accent mark above or below.
746#[derive(Debug)]
747pub struct AccentItem<'a> {
748    /// The base item.
749    pub base: MathItem<'a>,
750    /// The accent mark item.
751    pub accent: MathItem<'a>,
752    /// Whether this is a top or bottom accent.
753    pub position: Position,
754    /// Whether dotless styles have been added.
755    pub dotless: bool,
756    /// Whether the item's width should include the accent's width.
757    ///
758    /// Only used in paged export.
759    pub exact_frame_width: bool,
760}
761
762impl<'a> AccentItem<'a> {
763    /// Creates a new accent item.
764    ///
765    /// The resulting item inherits its math class from the base.
766    pub(crate) fn create(
767        base: MathItem<'a>,
768        accent: MathItem<'a>,
769        position: Position,
770        dotless: bool,
771        exact_frame_width: bool,
772        styles: StyleChain<'a>,
773    ) -> MathItem<'a> {
774        let props = MathProperties::new(styles, base.raw_class(), Span::detached());
775        let kind = MathKind::Accent(Box::new(Self {
776            base,
777            accent,
778            position,
779            dotless,
780            exact_frame_width,
781        }));
782        MathComponent { kind, props, styles }.into()
783    }
784}
785
786/// A base with a line overlaid.
787#[derive(Debug)]
788pub struct CancelItem<'a> {
789    /// The base item.
790    pub base: MathItem<'a>,
791    /// The length of the line.
792    pub length: Rel<Abs>,
793    /// The stroke for the line.
794    pub stroke: FixedStroke,
795    /// Whether a cross (two lines) is drawn instead of a single line.
796    pub cross: bool,
797    /// Whether to invert the angle of the first line.
798    pub invert_first_line: bool,
799    /// The angle of the line.
800    pub angle: Smart<CancelAngle>,
801}
802
803impl<'a> CancelItem<'a> {
804    /// Creates a new cancel item.
805    ///
806    /// The resulting item inherits its math class from the base.
807    pub(crate) fn create(
808        base: MathItem<'a>,
809        length: Rel<Abs>,
810        stroke: FixedStroke,
811        cross: bool,
812        invert_first_line: bool,
813        angle: Smart<CancelAngle>,
814        styles: StyleChain<'a>,
815        span: Span,
816    ) -> MathItem<'a> {
817        let props = MathProperties::new(styles, base.raw_class(), span);
818        let kind = MathKind::Cancel(Box::new(Self {
819            base,
820            length,
821            stroke,
822            cross,
823            invert_first_line,
824            angle,
825        }));
826        MathComponent { kind, props, styles }.into()
827    }
828}
829
830/// A base with a line drawn above or below.
831#[derive(Debug)]
832pub struct LineItem<'a> {
833    /// The base item.
834    pub base: MathItem<'a>,
835    /// Whether the line is drawn above or below the base.
836    pub position: Position,
837}
838
839impl<'a> LineItem<'a> {
840    /// Creates a new line item.
841    ///
842    /// The resulting item inherits its math class from the base.
843    pub(crate) fn create(
844        base: MathItem<'a>,
845        position: Position,
846        styles: StyleChain<'a>,
847        span: Span,
848    ) -> MathItem<'a> {
849        let props = MathProperties::new(styles, base.raw_class(), span);
850        let kind = MathKind::Line(Box::new(Self { base, position }));
851        MathComponent { kind, props, styles }.into()
852    }
853}
854
855/// The prime character used by [`PrimesItem`].
856pub const PRIME_CHAR: char = '′';
857
858/// Grouped prime symbols.
859///
860/// This is for more than four prime symbols, since there are only dedicated
861/// Unicode codepoints up to four.
862#[derive(Debug)]
863pub struct PrimesItem {
864    /// The number of primes to display. Always at least five.
865    pub count: usize,
866}
867
868impl PrimesItem {
869    /// Creates a new primes item.
870    pub(crate) fn create<'a>(count: usize, styles: StyleChain<'a>) -> MathItem<'a> {
871        let kind = MathKind::Primes(Box::new(Self { count }));
872        let props = MathProperties::default(styles, Span::detached());
873        MathComponent { kind, props, styles }.into()
874    }
875}
876
877/// A text string.
878#[derive(Debug)]
879pub struct TextItem<'a> {
880    /// The text content.
881    pub text: EcoString,
882    /// The item's locator.
883    pub locator: Locator<'a>,
884}
885
886impl<'a> TextItem<'a> {
887    /// Creates a new text item.
888    ///
889    /// The resulting item is spaced and has alphabetic math class.
890    pub(crate) fn create(
891        text: EcoString,
892        styles: StyleChain<'a>,
893        span: Span,
894        locator: Locator<'a>,
895    ) -> MathItem<'a> {
896        let kind = MathKind::Text(Self { text, locator });
897        let props = MathProperties::new(styles, Some(MathClass::Alphabetic), span)
898            .with_spaced(true);
899        MathComponent { kind, props, styles }.into()
900    }
901}
902
903/// A number.
904#[derive(Debug)]
905pub struct NumberItem {
906    /// The number's text content.
907    pub text: EcoString,
908}
909
910impl NumberItem {
911    /// Creates a new number item.
912    pub(crate) fn create<'a>(
913        text: EcoString,
914        styles: StyleChain<'a>,
915        span: Span,
916    ) -> MathItem<'a> {
917        let kind = MathKind::Number(Self { text });
918        let props = MathProperties::default(styles, span);
919        MathComponent { kind, props, styles }.into()
920    }
921}
922
923/// A single glyph (grapheme cluster).
924#[derive(Debug)]
925pub struct GlyphItem {
926    /// The text content.
927    pub text: EcoString,
928    /// The math class to use for layout.
929    ///
930    /// When the math class is large, the glyph is centered vertically and, in
931    /// display style, stretched vertically. This value is not necessarily the
932    /// same as the item's associated `MathProperties::class`, which is used
933    /// for determining spacing between items.
934    pub class: MathClass,
935    /// How the glyph should be stretched.
936    pub stretch: Cell<Stretch>,
937    /// Whether this glyph has been stretched as a middle delimiter.
938    pub mid_stretched: Cell<Option<bool>>,
939    /// Whether to apply the flac OpenType feature.
940    pub flac: Cell<bool>,
941}
942
943impl GlyphItem {
944    /// Creates a new glyph item.
945    ///
946    /// The `dtls` parameter indicates that a dotless character was converted
947    /// to its non-dotless version.
948    pub(crate) fn create<'a>(
949        text: EcoString,
950        styles: StyleChain<'a>,
951        span: Span,
952    ) -> MathItem<'a> {
953        assert!(text.graphemes(true).count() == 1);
954
955        let c = text.chars().next().unwrap();
956
957        let class = default_math_class(c);
958        let limits = Limits::for_char_with_class(c, class);
959
960        let kind = MathKind::Glyph(Box::new(Self {
961            text,
962            class: class.unwrap_or(MathClass::Normal),
963            stretch: Cell::new(Stretch::new()),
964            mid_stretched: Cell::new(None),
965            flac: Cell::new(false),
966        }));
967        let props = MathProperties::new(styles, class, span).with_limits(limits);
968        MathComponent { kind, props, styles }.into()
969    }
970}
971
972/// Inline content.
973#[derive(Debug)]
974pub struct BoxItem<'a> {
975    /// The [`BoxElem`] to layout.
976    pub elem: &'a Packed<BoxElem>,
977    /// The item's locator.
978    pub locator: Locator<'a>,
979}
980
981impl<'a> BoxItem<'a> {
982    /// Creates a new box item.
983    ///
984    /// The resulting item is spaced.
985    pub(crate) fn create(
986        elem: &'a Packed<BoxElem>,
987        styles: StyleChain<'a>,
988        locator: Locator<'a>,
989    ) -> MathItem<'a> {
990        let kind = MathKind::Box(Self { elem, locator });
991        let props = MathProperties::default(styles, elem.span()).with_spaced(true);
992        MathComponent { kind, props, styles }.into()
993    }
994}
995
996/// A MathML HTML element with resolved children.
997#[derive(Debug)]
998pub struct MathmlItem<'a> {
999    /// The original MathML HTML element content.
1000    ///
1001    /// This is always a `HtmlElem`.
1002    pub elem: &'a Content,
1003    /// The element's resolved IR body.
1004    pub body: Option<MathItem<'a>>,
1005}
1006
1007impl<'a> MathmlItem<'a> {
1008    /// Creates a new MathML HTML element item.
1009    pub(crate) fn create(
1010        elem: &'a Content,
1011        body: Option<MathItem<'a>>,
1012        styles: StyleChain<'a>,
1013    ) -> MathItem<'a> {
1014        let kind = MathKind::Mathml(Box::new(Self { elem, body }));
1015        let props = MathProperties::default(styles, elem.span());
1016        MathComponent { kind, props, styles }.into()
1017    }
1018}
1019
1020/// External content that needs to be laid out separately.
1021#[derive(Debug)]
1022pub struct ExternalItem<'a> {
1023    /// The content to layout externally.
1024    pub content: &'a Content,
1025    /// The item's locator.
1026    pub locator: Locator<'a>,
1027}
1028
1029impl<'a> ExternalItem<'a> {
1030    /// Creates a new external item.
1031    ///
1032    /// The resulting item is spaced and, if the content is a [`PlaceElem`], is
1033    /// ignorant.
1034    pub(crate) fn create(
1035        content: &'a Content,
1036        styles: StyleChain<'a>,
1037        locator: Locator<'a>,
1038    ) -> MathItem<'a> {
1039        let kind = MathKind::External(Self { content, locator });
1040        let props = MathProperties::default(styles, content.span())
1041            .with_spaced(true)
1042            .with_ignorant(content.is::<PlaceElem>());
1043        MathComponent { kind, props, styles }.into()
1044    }
1045}
1046
1047/// Shared sizing information for split fence segments.
1048#[derive(Debug)]
1049pub struct SharedFenceSizing<'a> {
1050    /// The body items of all fence segments.
1051    items: Vec<MathItem<'a>>,
1052    /// Relative to height for stretch size calculation.
1053    relative_to: Cell<Option<Abs>>,
1054    /// The fence's styles.
1055    styles: StyleChain<'a>,
1056}
1057
1058impl<'a> SharedFenceSizing<'a> {
1059    /// Creates a new shared sizing information.
1060    pub(crate) fn new(items: Vec<MathItem<'a>>, styles: StyleChain<'a>) -> Rc<Self> {
1061        Rc::new(Self { items, relative_to: Cell::new(None), styles })
1062    }
1063
1064    /// Retrieves or sets the relative to height by applying `f` to the body
1065    /// items.
1066    pub fn try_get_or_update(
1067        &self,
1068        f: impl FnOnce(&[MathItem<'a>], StyleChain<'a>) -> SourceResult<Abs>,
1069    ) -> SourceResult<Abs> {
1070        Ok(if let Some(relative_to) = self.relative_to.get() {
1071            relative_to
1072        } else {
1073            let relative_to = f(&self.items, self.styles)?;
1074            self.relative_to.set(Some(relative_to));
1075            relative_to
1076        })
1077    }
1078}
1079
1080/// The body of a [`FencedItem`].
1081#[derive(Debug)]
1082pub enum FencedBody<'a> {
1083    /// Owned body.
1084    Owned(MathItem<'a>),
1085    /// Shared body stored in [`SharedFenceSizing`].
1086    Shared { index: usize, sizing: Rc<SharedFenceSizing<'a>> },
1087}
1088
1089impl<'a> FencedBody<'a> {
1090    pub(crate) fn shared(index: usize, sizing: Rc<SharedFenceSizing<'a>>) -> Self {
1091        Self::Shared { index, sizing }
1092    }
1093
1094    /// Shared sizing info for split fence segments.
1095    pub fn sizing(&self) -> Option<&SharedFenceSizing<'a>> {
1096        match self {
1097            Self::Owned(_) => None,
1098            Self::Shared { sizing, .. } => Some(sizing),
1099        }
1100    }
1101}
1102
1103impl<'a> From<MathItem<'a>> for FencedBody<'a> {
1104    fn from(item: MathItem<'a>) -> Self {
1105        Self::Owned(item)
1106    }
1107}
1108
1109impl<'a> Deref for FencedBody<'a> {
1110    type Target = MathItem<'a>;
1111
1112    fn deref(&self) -> &Self::Target {
1113        match self {
1114            Self::Owned(item) => item,
1115            Self::Shared { index, sizing } => &sizing.items[*index],
1116        }
1117    }
1118}
1119
1120/// Stretch configuration for a glyph on both axes.
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1122pub struct Stretch(Axes<Option<StretchInfo>>);
1123
1124impl Stretch {
1125    /// Creates a new empty stretch configuration.
1126    pub(crate) fn new() -> Self {
1127        Self(Axes::splat(None))
1128    }
1129
1130    /// Adds horizontal stretch information.
1131    pub(crate) fn with_x(mut self, info: StretchInfo) -> Self {
1132        self.0.x = Some(info);
1133        self
1134    }
1135
1136    /// Adds vertical stretch information.
1137    pub(crate) fn with_y(mut self, info: StretchInfo) -> Self {
1138        self.0.y = Some(info);
1139        self
1140    }
1141
1142    /// Updates stretch info for both axes, combining with existing info and
1143    /// marking them as explicit.
1144    pub(crate) fn update(mut self, mut info: StretchInfo) -> Self {
1145        info.explicit = true;
1146        match &mut self.0.x {
1147            Some(val) => *val *= info,
1148            None => self.0.x = Some(info),
1149        }
1150        match &mut self.0.y {
1151            Some(val) => *val *= info,
1152            None => self.0.y = Some(info),
1153        }
1154        self
1155    }
1156
1157    /// Sets the reference size for relative stretching on the given axis.
1158    ///
1159    /// Only sets the value if not already set.
1160    pub(crate) fn relative_to(mut self, relative_to: Abs, axis: Axis) -> Self {
1161        if let Some(info) = self.0.get_mut(axis)
1162            && info.relative_to.is_none()
1163        {
1164            info.relative_to = Some(relative_to);
1165        }
1166        self
1167    }
1168
1169    /// Sets the font size for short-fall calculations on the given axis.
1170    ///
1171    /// Only sets the value if not already set.
1172    pub(crate) fn font_size(mut self, font_size: Abs, axis: Axis) -> Self {
1173        if let Some(info) = self.0.get_mut(axis)
1174            && info.font_size.is_none()
1175        {
1176            info.font_size = Some(font_size);
1177        }
1178        self
1179    }
1180
1181    /// Returns the stretch info for the given axis, if any.
1182    pub fn resolve(mut self, axis: Axis) -> Option<StretchInfo> {
1183        if let Some(info) = self.0.get_mut(axis)
1184            && let Some(buffer) = info.buffer
1185        {
1186            // Sort out the buffer before returning the info to use.
1187            if info.relative_to.is_some() {
1188                info.target = buffer;
1189            } else {
1190                info.target = Rel::new(
1191                    info.target.rel * buffer.rel,
1192                    buffer.rel.of(info.target.abs) + buffer.abs,
1193                );
1194            }
1195        }
1196        self.0.get(axis)
1197    }
1198
1199    /// Returns the user-requested stretch target for the given axis, if any.
1200    pub fn resolve_requested(self, axis: Axis) -> Option<Rel<Length>> {
1201        self.0
1202            .get(axis)
1203            .and_then(|info| info.requested_target)
1204            .filter(|target| !target.is_one())
1205    }
1206
1207    /// Whether the stretch along the given axis should be represented
1208    /// explicitly.
1209    pub fn is_explicit(self, axis: Axis) -> bool {
1210        self.0.get(axis).is_some_and(|info| info.explicit)
1211    }
1212}
1213
1214/// Information about how to stretch a glyph on one axis.
1215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1216pub struct StretchInfo {
1217    /// The target size to stretch to.
1218    pub target: Rel<Abs>,
1219    /// A buffer to store the latest stretch added, in case it needs to be
1220    /// relative to something else.
1221    buffer: Option<Rel<Abs>>,
1222    /// Whether this stretch is explicit. That is, the stretch was not from a
1223    /// large operator in display math.
1224    pub(crate) explicit: bool,
1225    /// The user-requested stretch target, if any.
1226    pub(crate) requested_target: Option<Rel<Length>>,
1227    /// The short-fall amount for glyph assembly.
1228    pub short_fall: Em,
1229    /// The reference size for relative targets.
1230    ///
1231    /// Only used in paged export.
1232    pub relative_to: Option<Abs>,
1233    /// The font size to use for short-fall.
1234    ///
1235    /// Only used in paged export.
1236    pub font_size: Option<Abs>,
1237}
1238
1239impl StretchInfo {
1240    /// Creates new stretch info with the given target and short-fall.
1241    pub(crate) fn new(target: Rel<Abs>, short_fall: Em) -> Self {
1242        Self {
1243            target,
1244            buffer: None,
1245            explicit: false,
1246            requested_target: None,
1247            short_fall,
1248            relative_to: None,
1249            font_size: None,
1250        }
1251    }
1252
1253    /// Creates stretch info from a user-specified size.
1254    pub(crate) fn from_size(size: Rel<Length>, short_fall: Em, font_size: Abs) -> Self {
1255        Self {
1256            target: size.map(|l| l.at(font_size)),
1257            buffer: None,
1258            explicit: false,
1259            requested_target: (!size.is_one()).then_some(size),
1260            short_fall,
1261            relative_to: None,
1262            font_size: None,
1263        }
1264    }
1265}
1266
1267impl Default for StretchInfo {
1268    fn default() -> Self {
1269        let target = Rel::new(Ratio::one(), Abs::zero());
1270        Self::new(target, Em::zero())
1271    }
1272}
1273
1274impl MulAssign for StretchInfo {
1275    fn mul_assign(&mut self, rhs: Self) {
1276        if let Some(buffer) = self.buffer {
1277            self.target = Rel::new(
1278                self.target.rel * buffer.rel,
1279                buffer.rel.of(self.target.abs) + buffer.abs,
1280            );
1281        }
1282        self.buffer = Some(rhs.target);
1283
1284        if let Some(requested) = rhs.requested_target {
1285            self.requested_target =
1286                Some(self.requested_target.map_or(requested, |target| {
1287                    Rel::new(
1288                        target.rel * requested.rel,
1289                        requested.rel.of(target.abs) + requested.abs,
1290                    )
1291                }));
1292        }
1293
1294        self.explicit = self.explicit || rhs.explicit;
1295        self.short_fall = rhs.short_fall;
1296    }
1297}
1298
1299/// A marker representing the positioning of something above or below a base.
1300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1301pub enum Position {
1302    /// Placed above the base.
1303    Above,
1304    /// Placed below the base.
1305    Below,
1306}