Skip to main content

fmd_math/
node.rs

1//! The parse tree: [`Node`] and its supporting vocabulary.
2//!
3//! Every node carries its **byte span** into the source string — span
4//! provenance is structural (the G0-3 ratification's §11.3 requirement), not
5//! an afterthought: the span map that downstream consumers (`isolate`,
6//! `tex_to_color_map`, `TransformMatchingTex`) use is derived from these
7//! spans, so no node may ever be constructed without one.
8
9use crate::atom::AtomClass;
10use crate::style::Style;
11
12/// A half-open byte range `[start, end)` into the source string.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct Span {
15    /// Byte offset of the first byte of the construct.
16    pub start: usize,
17    /// Byte offset one past the last byte of the construct.
18    pub end: usize,
19}
20
21impl Span {
22    /// Construct a span. `start` and `end` are byte offsets; `end >= start`.
23    #[must_use]
24    pub const fn new(start: usize, end: usize) -> Self {
25        Self { start, end }
26    }
27
28    /// The smallest span covering both `self` and `other`.
29    #[must_use]
30    pub fn union(self, other: Self) -> Self {
31        Self {
32            start: self.start.min(other.start),
33            end: self.end.max(other.end),
34        }
35    }
36
37    /// Length in bytes.
38    #[must_use]
39    pub const fn len(&self) -> usize {
40        self.end.saturating_sub(self.start)
41    }
42
43    /// True when the span covers zero bytes.
44    #[must_use]
45    pub const fn is_empty(&self) -> bool {
46        self.end <= self.start
47    }
48}
49
50/// One parse-tree node: a kind plus the byte span it came from.
51#[derive(Clone, Debug, PartialEq)]
52pub struct Node {
53    /// What the node is.
54    pub kind: NodeKind,
55    /// Where in the source string it came from.
56    pub span: Span,
57}
58
59impl Node {
60    /// Construct a node.
61    #[must_use]
62    pub const fn new(kind: NodeKind, span: Span) -> Self {
63        Self { kind, span }
64    }
65}
66
67/// A delimiter as named after `\left`, `\right`, or a `\big`-class command.
68///
69/// `ch` is `None` for the null delimiter `.` (as in `\right.`).
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Delim {
72    /// The delimiter character (already mapped to its math codepoint, e.g.
73    /// `\langle` ⇒ `⟨`), or `None` for the null delimiter.
74    pub ch: Option<char>,
75    /// Source span of the delimiter token.
76    pub span: Span,
77}
78
79/// The four fixed delimiter sizes of the `\big` family.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
81pub enum DelimSize {
82    /// `\big` class — 8.5 pt-per-10 pt nominal.
83    Big,
84    /// `\Big` class.
85    BBig,
86    /// `\bigg` class.
87    Bigg,
88    /// `\Bigg` class.
89    BBigg,
90}
91
92/// How a big operator places its scripts.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
94pub enum Limits {
95    /// TeX's default: limits in display style for `\sum`-class operators,
96    /// side scripts otherwise; `\int`-class operators default to side
97    /// scripts in every style.
98    #[default]
99    Default,
100    /// `\limits`: scripts above/below regardless of style.
101    Limits,
102    /// `\nolimits`: side scripts regardless of style.
103    NoLimits,
104}
105
106/// The generalized-fraction flavors (rule 15's inputs).
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct FracSpec {
109    /// Draw the fraction bar.
110    pub bar: bool,
111    /// Delimiters wrapped around the whole fraction (`\binom`/`\choose`
112    /// carry `( )`); `None` for plain fractions.
113    pub delims: Option<(char, char)>,
114    /// A forced layout style (`\dfrac` forces display, `\tfrac` text);
115    /// `None` follows the ambient style.
116    pub forced_style: Option<Style>,
117}
118
119/// The accent commands (both true accents and the wide over/under class).
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum AccentKind {
122    /// `\hat`
123    Hat,
124    /// `\check`
125    Check,
126    /// `\tilde`
127    Tilde,
128    /// `\acute`
129    Acute,
130    /// `\grave`
131    Grave,
132    /// `\dot`
133    Dot,
134    /// `\ddot`
135    Ddot,
136    /// `\breve`
137    Breve,
138    /// `\bar`
139    Bar,
140    /// `\vec`
141    Vec,
142    /// `\dddot` — amsmath builds it from three dot marks in a row; so does
143    /// the layout here (no bundled face carries U+20DB).
144    Dddot,
145    /// `\ddddot` — four dot marks, same construction.
146    Ddddot,
147    /// `\mathring`
148    Ring,
149    /// `\widehat`
150    WideHat,
151    /// `\widetilde`
152    WideTilde,
153    /// `\overline`
154    OverLine,
155    /// `\underline` (math mode; in text mode `\underline` is a
156    /// [`TextStyle::Underline`] island)
157    UnderLine,
158    /// `\overbrace` (annotations attach as scripts on the wrapping
159    /// [`NodeKind::Scripts`] node, exactly as TeX attaches them)
160    OverBrace,
161    /// `\underbrace`
162    UnderBrace,
163    /// `\overrightarrow`
164    OverRightArrow,
165    /// `\overleftarrow`
166    OverLeftArrow,
167}
168
169impl AccentKind {
170    /// True for accents that sit above the base (everything except the
171    /// under-class accents).
172    #[must_use]
173    pub const fn is_over(self) -> bool {
174        !matches!(self, Self::UnderLine | Self::UnderBrace)
175    }
176}
177
178/// The text-mode styling islands of the TexText contract.
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum TextStyle {
181    /// `\textbf{…}`
182    Bold,
183    /// `\emph{…}`
184    Emph,
185    /// `\underline{…}` in text mode
186    Underline,
187}
188
189/// The argument-taking math alphabet commands.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum MathFont {
192    /// `\mathbb` (and `\mathds`, which the default preamble pack maps here)
193    Blackboard,
194    /// `\mathcal`
195    Calligraphic,
196    /// `\mathrm`
197    Roman,
198    /// `\mathbf`
199    Bold,
200    /// `\boldsymbol`
201    BoldItalic,
202    /// `\mathsf`
203    SansSerif,
204    /// `\mathtt`
205    Typewriter,
206    /// `\mathit`
207    Italic,
208}
209
210/// The explicit spacing commands.
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum SpaceKind {
213    /// `\,` — 3 mu
214    Thin,
215    /// `\:` — 4 mu
216    Med,
217    /// `\;` — 5 mu
218    Thick,
219    /// `\!` — −3 mu
220    NegThin,
221    /// `\quad` — 18 mu (1 em)
222    Quad,
223    /// `\qquad` — 36 mu (2 em)
224    Qquad,
225    /// `\ ` (control space) — an ordinary interword space
226    ControlSpace,
227}
228
229impl SpaceKind {
230    /// The width in mu (18 mu = 1 em at the current size). The control
231    /// space is nominally a text interword space; 6 mu (= ⅓ em) is the
232    /// conventional math approximation.
233    #[must_use]
234    pub const fn mu(self) -> i32 {
235        match self {
236            Self::Thin => 3,
237            Self::Med => 4,
238            Self::Thick => 5,
239            Self::NegThin => -3,
240            Self::Quad => 18,
241            Self::Qquad => 36,
242            Self::ControlSpace => 6,
243        }
244    }
245}
246
247/// The phantom flavors.
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub enum PhantomKind {
250    /// `\phantom` — occupies width, height, and depth.
251    Full,
252    /// `\hphantom` — occupies width only.
253    Horizontal,
254    /// `\vphantom` — occupies height and depth only.
255    Vertical,
256}
257
258/// The `\stackrel`/`\overset`/`\underset` family.
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260pub enum StackKind {
261    /// `\stackrel{top}{base}` — the result is a Rel atom.
262    Stackrel,
263    /// `\overset{top}{base}` — the result takes the base's class.
264    Overset,
265    /// `\underset{bottom}{base}` — the result takes the base's class.
266    Underset,
267}
268
269/// What a node is. See the module docs; every variant is produced by
270/// [`crate::parse`] / [`crate::parse_text`] with full span provenance.
271#[derive(Clone, Debug, PartialEq)]
272pub enum NodeKind {
273    /// A horizontal list: a group's content, a cell, an argument, or the
274    /// whole formula.
275    List(Vec<Node>),
276    /// A single character atom, already mapped to its math codepoint
277    /// (`-` ⇒ `−`, `*` ⇒ `∗`, `\pi` ⇒ `π`). `class` is the intrinsic atom
278    /// class before contextual Bin→Ord degradation.
279    Symbol {
280        /// The (mapped) character.
281        ch: char,
282        /// Intrinsic atom class.
283        class: AtomClass,
284    },
285    /// A big operator (`\sum`, `\int`, …): an Op atom with a limits mode.
286    BigOp {
287        /// The operator character (`∑`, `∫`, …).
288        ch: char,
289        /// `\limits`/`\nolimits` state.
290        limits: Limits,
291        /// True for the `\int` class, whose default is side scripts even
292        /// in display style.
293        integral: bool,
294    },
295    /// A roman operator name (`\sin`, `\lim`, `\operatorname{…}`): an Op
296    /// atom set in upright text.
297    OpName {
298        /// The rendered name ("sin", "lim", …).
299        name: String,
300        /// True for the `\lim` class, which takes under/over scripts in
301        /// display style.
302        limits: bool,
303    },
304    /// Sub/superscripts and primes attached to a base atom. `base` is
305    /// `None` when the script opens the list (TeX's empty-nucleus atom).
306    Scripts {
307        /// The atom the scripts attach to.
308        base: Option<Box<Node>>,
309        /// Subscript.
310        sub: Option<Box<Node>>,
311        /// Superscript (primes precede it visually).
312        sup: Option<Box<Node>>,
313        /// The `'` primes, one source span each.
314        primes: Vec<Span>,
315    },
316    /// A generalized fraction: `\frac`-family, `\binom`/`\choose`, or an
317    /// infix `\over` that split its enclosing list.
318    Frac {
319        /// Numerator.
320        num: Box<Node>,
321        /// Denominator.
322        den: Box<Node>,
323        /// Bar/delimiter/style flavor.
324        spec: FracSpec,
325    },
326    /// `\sqrt`, with an optional index (`\sqrt[3]{x}`).
327    Radical {
328        /// The index, if any.
329        index: Option<Box<Node>>,
330        /// The radicand.
331        radicand: Box<Node>,
332    },
333    /// An accented atom.
334    Accent {
335        /// Which accent.
336        accent: AccentKind,
337        /// The base.
338        base: Box<Node>,
339    },
340    /// `\left … \right`: an Inner atom.
341    LeftRight {
342        /// Opening delimiter.
343        left: Delim,
344        /// Closing delimiter.
345        right: Delim,
346        /// The enclosed list.
347        body: Vec<Node>,
348    },
349    /// A fixed-size delimiter (`\big(`, `\Big\{`, …).
350    SizedDelim {
351        /// Which size.
352        size: DelimSize,
353        /// The atom class the variant imposes: `\bigl` ⇒ Open, `\bigr` ⇒
354        /// Close, `\bigm` ⇒ Rel, plain `\big` ⇒ Ord.
355        class: AtomClass,
356        /// The delimiter.
357        delim: Delim,
358    },
359    /// `\text{…}` inside mathematics: the body is text-mode content.
360    Text {
361        /// Text-mode body.
362        body: Vec<Node>,
363    },
364    /// A literal run of text-mode characters, with one source span per
365    /// character (decoded characters and source bytes are not linear:
366    /// escapes decode two bytes to one character, whitespace collapses) —
367    /// the provenance `Text[3:7]`-style slicing consumes.
368    TextRun {
369        /// The decoded text.
370        text: String,
371        /// One span per `char` of `text`, in order.
372        char_spans: Vec<Span>,
373    },
374    /// A `\textbf`/`\emph`/`\underline` styling island (text mode, or the
375    /// LaTeX text-in-math form).
376    TextStyled {
377        /// Which style.
378        style: TextStyle,
379        /// The body, in text mode.
380        body: Vec<Node>,
381    },
382    /// `$…$` (or `$$…$$`) inside text mode: the body is math-mode
383    /// content.
384    MathIsland {
385        /// Math-mode body.
386        body: Vec<Node>,
387        /// True for `$$…$$` display mathematics (lays out in display
388        /// style); false for inline `$…$` (text style).
389        display: bool,
390    },
391    /// A style-switch marker (`\displaystyle` …) applying to the remainder
392    /// of the enclosing list.
393    StyleChange(Style),
394    /// A line-alignment declaration (`\centering`): applies to the
395    /// remainder of the enclosing list, exiting at group end, exactly like
396    /// LaTeX's paragraph declarations. Produces no glyphs of its own.
397    AlignChange(LineAlign),
398    /// A size declaration (`\small`, `\Large`, …): sets the current size
399    /// factor for the remainder of the enclosing list, exiting at group
400    /// end, and composing multiplicatively with the script styles (the
401    /// LaTeX 10 pt ladder of `size10.clo`).
402    SizeChange(f64),
403    /// A `\color{…}` marker applying to the remainder of the enclosing
404    /// group. The argument is kept verbatim.
405    ColorChange(String),
406    /// A line-spacing declaration (`\doublespacing`): multiplies the
407    /// \baselineskip of subsequent `\\`-stacked lines in the enclosing
408    /// multi-line list (setspace's \baselinestretch, 10 pt ladder value).
409    /// Produces no glyphs; inert inside grids, whose row rule is the
410    /// environment's own.
411    LineSpacing(f64),
412    /// A math-alphabet command applied to one argument.
413    MathFont {
414        /// Which alphabet.
415        font: MathFont,
416        /// The argument.
417        body: Box<Node>,
418    },
419    /// A phantom box.
420    Phantom {
421        /// Which dimensions it occupies.
422        kind: PhantomKind,
423        /// The hidden body.
424        body: Box<Node>,
425    },
426    /// `\stackrel`/`\overset`/`\underset`.
427    Stack {
428        /// Which flavor.
429        kind: StackKind,
430        /// The small stacked element (top for stackrel/overset, bottom for
431        /// underset).
432        annotation: Box<Node>,
433        /// The base.
434        base: Box<Node>,
435    },
436    /// `\xrightarrow[below]{above}` / `\xmapsto[below]{above}`: a drawn
437    /// arrow stretched to its script-style labels, spaced as a relation.
438    XArrow {
439        /// True for `\xmapsto` (the origin-bar band).
440        mapsto: bool,
441        /// The mandatory above-label (a `List` node).
442        above: Box<Node>,
443        /// The optional below-label (a `List` node).
444        below: Option<Box<Node>>,
445    },
446    /// An explicit spacing command.
447    Space(SpaceKind),
448    /// `~` — a tie (non-breaking interword space).
449    Tie,
450    /// `\\` — a line break (or, at an environment's own level, the row
451    /// separator, in which case it is consumed by the environment).
452    Linebreak,
453    /// `&` — an alignment tab (or, at an environment's own level, the cell
454    /// separator, in which case it is consumed by the environment). Kept as
455    /// a node at the top level because the Tex surface wraps whole strings
456    /// in an `align*`-class environment.
457    AlignTab,
458    /// A line-alignment environment (`flushleft`, `center`, `flushright`):
459    /// a text-mode block whose `\\`-split lines are aligned within the
460    /// widest line's width. Lines are [`NodeKind::List`] nodes.
461    AlignBlock {
462        /// The line alignment.
463        align: LineAlign,
464        /// The lines, in order.
465        lines: Vec<Node>,
466    },
467    /// A `\begin{name} … \end{name}` environment. Cells are
468    /// [`NodeKind::List`] nodes.
469    Environment {
470        /// Environment name (`array`, `cases`, …).
471        name: String,
472        /// The column-spec argument (`array` only), kept verbatim.
473        spec: Option<String>,
474        /// Rows of cells.
475        rows: Vec<Vec<Node>>,
476    },
477    /// A structural fragment: the Tex surface's multi-argument idiom makes
478    /// each literal argument its own corpus string, so a piece may be a
479    /// *substring of a balanced whole* (`"{a"`, `"b}"`, `"\right)"`). The
480    /// grammar accepts these at the top level and marks them explicitly —
481    /// never silently.
482    Fragment(FragmentKind),
483}
484
485/// How the lines of a multi-line block are aligned horizontally within
486/// the block's width (the widest line's width).
487#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
488pub enum LineAlign {
489    /// `flushleft` (and the default): every line flush left.
490    #[default]
491    Left,
492    /// `center` / `\centering`: every line centered.
493    Center,
494    /// `flushright`: every line flush right.
495    Right,
496}
497
498impl LineAlign {
499    /// The fraction of a line's slack placed to its left: `0` flush left,
500    /// `1/2` centered, `1` flush right.
501    #[must_use]
502    pub fn slack_factor(self) -> f64 {
503        match self {
504            Self::Left => 0.0,
505            Self::Center => 0.5,
506            Self::Right => 1.0,
507        }
508    }
509}
510
511/// The structural fragments the top level tolerates (per-argument
512/// `SingleStringTex` semantics).
513#[derive(Clone, Debug, PartialEq)]
514pub enum FragmentKind {
515    /// An unmatched `}` whose opener lives in an earlier piece. Transparent
516    /// to classification and spacing; renders nothing.
517    UnmatchedClose,
518    /// A `\right` whose `\left` lives in an earlier piece; renders its
519    /// delimiter (class Close).
520    StrayRight(Delim),
521    /// A redundant `$` in a math-mode string (authors wrapping an
522    /// already-math string in dollars). Transparent; renders nothing.
523    RedundantMathShift,
524}