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}