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