Skip to main content

fmd_math/
lib.rs

1//! # fmd-math — the clean-room TeX-mathematics layout engine
2//!
3//! A math-*layout* engine in the KaTeX/Typst class, not a TeX macro
4//! processor: the TeX mathematics grammar as actually used, TeX's published
5//! layout rules (the eight atom classes, the inter-atom spacing table,
6//! display/text/script/scriptscript style propagation), and — with the
7//! placement beads — Appendix-G construction mathematics over metrics
8//! calibrated for the bundled faces.
9//!
10//! This crate is contributed and consumed by the franken_manim program (its
11//! Scribe subsystem typesets `Tex`/`TexText` through it; see that repo's
12//! `UPSTREAM_LEDGER.md` row 2) and serves fmd's own native `$…$`
13//! mathematics. The public API is **frozen to the shape recorded in
14//! franken_manim's `docs/g0/G0-3-fmd-math-ratification.md`** until its G2
15//! gate.
16//!
17//! ## The pipeline
18//!
19//! ```text
20//! source &str
21//!   → parse           tokens → Node tree; EVERY node carries its byte span
22//!   → classify        the eight atom classes; Bin→Ord degradation in context
23//!   → layout(Ctx)     style (D/T/S/SS × cramped) threaded top-down;
24//!                     Appendix-G constructions build boxes bottom-up
25//!   → Layout          positioned {glyphs, rules, drawn paths}, each glyph
26//!                     naming its FACE and carrying its source span
27//! ```
28//!
29//! This crate release carries the front of the pipeline: [`parse`] /
30//! [`parse_text`], the atom engine ([`atom`]), the style machinery
31//! ([`style`]), and the fixed node model plus output types ([`Layout`] and
32//! friends). `Engine::new(faces…)` / `typeset` land with the placement
33//! bead on exactly these shapes.
34//!
35//! ## Modes
36//!
37//! [`parse`] reads a whole string as mathematics (the `Tex` surface; `&`
38//! and `\\` are legal at top level because the Reference wraps whole
39//! strings in an `align*`-class environment). [`parse_text`] reads the
40//! TexText contract: a text mainland with `$…$` math islands,
41//! `\textbf`/`\emph`/`\underline`, and the escape set.
42//!
43//! ## Fragment tolerance (per-argument `SingleStringTex` semantics)
44//!
45//! The Tex surface's multi-argument idiom makes each literal argument its
46//! own string, so a string may legitimately be a *piece of a balanced
47//! whole* (`"{a"`, `"\over"`, `"b}"`, `"\left("`, `"a^"`). The grammar
48//! therefore (a) lets **end of input close whatever is open** — groups,
49//! `\left`, `$` islands, arguments still to come (they become empty
50//! lists) — and (b) accepts, at the top level, an unmatched `}`, a stray
51//! `\right`, or a redundant `$` as an explicit
52//! [`node::FragmentKind`] marker, never silently. Mid-string structural
53//! faults (wrong closer, double scripts, `&` in prose) remain precise
54//! errors. In text mode, the Reference-era LaTeX missing-`$` recovery is
55//! kept deliberately: a self-contained math command or a bare script in
56//! the mainland becomes an explicit implicit [`NodeKind::MathIsland`],
57//! and `$$…$$` display mathematics is recognized.
58//!
59//! ## The span map (§11.3)
60//!
61//! Every output primitive carries its source byte span, exactly: text-run
62//! characters per character, primes per `'` token, command-produced
63//! glyphs the producing command's span (the expansion site). The
64//! [`spanmap`] module turns that provenance into the consumption surface:
65//! [`spanmap::find_occurrences`] + [`Layout::select`] (containment
66//! semantics) is the native replacement for the Reference's
67//! render-twice-and-align hack — `isolate`, `tex_to_color_map`, substring
68//! slicing, and `TransformMatchingTex` match by source identity.
69//!
70//! ## The error contract (the coverage ratchet's unit)
71//!
72//! There is deliberately no fallback typesetter, so coverage discipline
73//! replaces fallback discipline: an unsupported construct is a **precise,
74//! named error** ([`MathError::UnsupportedCommand`] with the construct's
75//! G0-4 table name and a tier tag in its message), and arbitrary input
76//! errors cleanly — never hangs, never garbles, never panics (the chaos
77//! suite locks this). [`construct_status`] answers, for any construct in
78//! the table's naming scheme, whether the parse surface supports it.
79
80#![forbid(unsafe_code)]
81
82pub mod atom;
83pub mod commands;
84mod drawn;
85mod error;
86pub mod faces;
87mod layout;
88pub mod macros;
89pub mod mathml;
90mod mbox;
91pub mod metrics;
92pub mod node;
93mod parse;
94pub mod paths;
95pub mod spanmap;
96pub mod style;
97mod token;
98
99pub use commands::{
100    ConstructStatus, LAYOUT_PENDING_TRACKING, TIER2_TRACKING, Tier, UNTIERED_TRACKING,
101    construct_status,
102};
103pub use error::MathError;
104pub use faces::FaceSet;
105pub use layout::Engine;
106pub use macros::MacroSet;
107pub use mathml::{mathml_well_formed, to_mathml, to_mathml_element, to_mathml_with_capacity};
108pub use mbox::{FaceId, Layout, PathContour, PathSeg, PlacedGlyph, PlacedPath, PlacedRule};
109pub use metrics::MathConstants;
110pub use node::{Node, NodeKind, Span};
111pub use spanmap::{Selection, find_occurrences};
112pub use style::{Style, StyleCtx, style_walk};
113
114/// Parse a whole source string as mathematics (the `Tex` surface). The
115/// result is a [`NodeKind::List`] node spanning the whole input; every
116/// descendant carries its byte span.
117///
118/// # Errors
119///
120/// [`MathError::UnsupportedCommand`] for constructs outside the implemented
121/// surface (named precisely, tier-tagged); [`MathError::Malformed`] for
122/// structural faults (unbalanced groups, double scripts, stray `\right`,
123/// over-deep nesting), with the byte offset of the offense.
124pub fn parse(source: &str) -> Result<Node, MathError> {
125    parse::parse_math(source)
126}
127
128/// Parse a whole source string under the TexText contract: a text mainland
129/// with `$…$` math islands.
130///
131/// # Errors
132///
133/// As [`parse`]; additionally, math-only material in the text mainland
134/// (`^`, `_`, `&`, math-mode commands) is a precise [`MathError::Malformed`]
135/// telling the caller to wrap it in `$…$`.
136pub fn parse_text(source: &str) -> Result<Node, MathError> {
137    parse::parse_text_mode(source)
138}
139
140/// [`parse`], against a macro set (a preamble pack and/or caller
141/// definitions): calls expand at the token level before the grammar runs,
142/// with body-produced tokens carrying their call site's span (see
143/// [`macros`]).
144///
145/// # Errors
146///
147/// As [`parse`], plus the macro-expansion errors ([`macros`]): recursion
148/// refusal, budget overruns, malformed definitions — every one precise.
149pub fn parse_with_macros(source: &str, macros: &MacroSet) -> Result<Node, MathError> {
150    parse::parse_math_with(source, macros)
151}
152
153/// [`parse_text`], against a macro set.
154///
155/// # Errors
156///
157/// As [`parse_with_macros`].
158pub fn parse_text_with_macros(source: &str, macros: &MacroSet) -> Result<Node, MathError> {
159    parse::parse_text_mode_with(source, macros)
160}