Skip to main content

rustyfi_backend/
math.rs

1//! Math value / box model: trimmed analog of `math.ml`'s `math_kind`
2//! (`horzBox.ml:134`) and `low_math_atom` (`math.ml:9`).
3
4use crate::hbox::HorzStringInfo;
5use crate::length::Length;
6use std::collections::BTreeMap;
7
8/// v0.0.6 `math_kind` (horzBox.ml:134-145). `Prefix` is a SATySFi-specific
9/// class (differential `d`, `\partial`); `End` is a synthetic list-boundary
10/// sentinel.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum MathKind {
13    Ord,
14    Bin,
15    Rel,
16    Op,
17    Punct,
18    Open,
19    Close,
20    Prefix,
21    Inner,
22    End,
23}
24
25/// One already-positioned glyph inside a laid-out math run: a string set in
26/// `info` (font + size — a superscript carries a *smaller* size here), placed
27/// at `dx` right of the math box's origin and `dy` **above** its baseline
28/// (dy < 0 = below, for subscripts). The analog of `math.ml`'s `LowMathGlyph`
29/// after `horz_of_low_math` has resolved its `PHGRising` shift into an
30/// offset.
31#[derive(Clone, Debug, PartialEq)]
32pub struct MathGlyph {
33    pub info: HorzStringInfo,
34    pub text: String,
35    /// `Some(gid)`: a raw MATH-table variant glyph id
36    /// (`push_big_char_glyph`/`push_delimiter_glyph`), NOT necessarily
37    /// cmap-reachable from `text`; the CID writer emits it directly rather
38    /// than re-deriving a gid, keeping `text` as the ToUnicode source. `None`
39    /// for every ordinary cmap-driven glyph, and on every base-14 path (that
40    /// writer never reads this field).
41    pub gid: Option<u16>,
42    pub dx: Length,
43    pub dy: Length,
44    pub width: Length,
45    pub height: Length,
46    pub depth: Length,
47}
48
49/// v0.0.6 `math_char_class` (`primitives.cppo.ml`'s `MathItalic`/…): which
50/// Mathematical-Alphanumeric style block a plain `${…}` letter resolves to
51/// (`\mathrm`/`\mathbf`/… — `math.satyh`'s `\math-style`). `Ord`/`Hash` so
52/// it can key `Context::math_variant_char_map`'s override table.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum MathCharClass {
55    Italic,
56    BoldItalic,
57    Roman,
58    BoldRoman,
59    Script,
60    BoldScript,
61    Fraktur,
62    BoldFraktur,
63    DoubleStruck,
64    /// Upstream dev-0-1-0 widens `math_char_class` 9 → 14
65    /// (`b836d512:src/backend/horzBox.ml:98-113`); v0.0.6 has exactly the 9
66    /// above (`v0.0.6:src/backend/horzBox.ml:147-158`). These 5 are
67    /// unreachable under V0_0: registration
68    /// (`prim_types.rs::math_char_class_decl`) is V0_1-gated and typecheck
69    /// rejects unregistered constructor names — version-blind at this
70    /// enum/backend layer, version-gated at the registration layer.
71    SansSerif,
72    BoldSansSerif,
73    ItalicSansSerif,
74    BoldItalicSansSerif,
75    Typewriter,
76}
77
78/// Upstream `default_math_class_map` (`primitives.cppo.ml:465-480`):
79/// whole-TOKEN entries consulted BEFORE the per-char variant lookup below;
80/// value = (replacement codepoints, math class). Only `-` changes codepoint
81/// (`-` -> U+2212 MINUS SIGN); every other entry is an identity remap that
82/// exists purely to attach the right `MathKind` to the whole run.
83pub(crate) fn default_math_class_map() -> BTreeMap<String, (String, MathKind)> {
84    [
85        ("=", "=", MathKind::Rel),
86        ("<", "<", MathKind::Rel),
87        (">", ">", MathKind::Rel),
88        (":", ":", MathKind::Rel),
89        ("+", "+", MathKind::Bin),
90        ("-", "\u{2212}", MathKind::Bin),
91        ("|", "|", MathKind::Bin),
92        ("/", "/", MathKind::Ord),
93        (",", ",", MathKind::Punct),
94    ]
95    .into_iter()
96    .map(|(k, v, mk)| (k.to_string(), (v.to_string(), mk)))
97    .collect()
98}
99
100/// Upstream `default_math_variant_char_map` (`primitives.cppo.ml:358-460`)
101/// as a pure function (base-offset + exception lists) rather than a big table
102/// cloned into every `Context`, which stores only the runtime override map
103/// (`set-math-variant-char`). `None` means "no remap" — either `class`/`c` has
104/// no Unicode Mathematical-Alphanumeric counterpart (e.g. a digit), or the
105/// letter has no assigned codepoint in that style block (a handful of
106/// Script/Fraktur/Double-Struck letters use a distinct legacy symbol instead,
107/// per Unicode's own gaps — the hardcoded exceptions below).
108pub fn default_math_variant_char(class: MathCharClass, c: char) -> Option<char> {
109    let cap = c.is_ascii_uppercase().then(|| c as u32 - 'A' as u32);
110    let small = c.is_ascii_lowercase().then(|| c as u32 - 'a' as u32);
111    fn cp(base: u32, i: u32) -> Option<char> {
112        char::from_u32(base + i)
113    }
114    match class {
115        MathCharClass::Italic => match (cap, small) {
116            (Some(i), _) => cp(0x1D434, i),
117            (_, Some(7)) => Some('\u{210E}'),
118            (_, Some(i)) => cp(0x1D44E, i),
119            _ => None,
120        },
121        MathCharClass::BoldItalic => match (cap, small) {
122            (Some(i), _) => cp(0x1D468, i),
123            (_, Some(i)) => cp(0x1D482, i),
124            _ => None,
125        },
126        MathCharClass::Roman => (cap.is_some() || small.is_some()).then_some(c),
127        MathCharClass::BoldRoman => match (cap, small) {
128            (Some(i), _) => cp(0x1D400, i),
129            (_, Some(i)) => cp(0x1D41A, i),
130            _ => None,
131        },
132        MathCharClass::Script => match c {
133            'B' => Some('\u{212C}'),
134            'E' => Some('\u{2130}'),
135            'F' => Some('\u{2131}'),
136            'H' => Some('\u{210B}'),
137            'I' => Some('\u{2110}'),
138            'L' => Some('\u{2112}'),
139            'M' => Some('\u{2133}'),
140            'R' => Some('\u{211B}'),
141            'e' => Some('\u{212F}'),
142            'g' => Some('\u{210A}'),
143            'o' => Some('\u{2134}'),
144            _ => match (cap, small) {
145                (Some(i), _) => cp(0x1D49C, i),
146                (_, Some(i)) => cp(0x1D4B6, i),
147                _ => None,
148            },
149        },
150        MathCharClass::BoldScript => match (cap, small) {
151            (Some(i), _) => cp(0x1D4D0, i),
152            (_, Some(i)) => cp(0x1D4EA, i),
153            _ => None,
154        },
155        MathCharClass::Fraktur => match c {
156            'C' => Some('\u{212D}'),
157            'H' => Some('\u{210C}'),
158            'I' => Some('\u{2111}'),
159            'R' => Some('\u{211C}'),
160            'Z' => Some('\u{2128}'),
161            _ => match (cap, small) {
162                (Some(i), _) => cp(0x1D504, i),
163                (_, Some(i)) => cp(0x1D51E, i),
164                _ => None,
165            },
166        },
167        MathCharClass::BoldFraktur => match (cap, small) {
168            (Some(i), _) => cp(0x1D56C, i),
169            (_, Some(i)) => cp(0x1D586, i),
170            _ => None,
171        },
172        MathCharClass::DoubleStruck => match c {
173            'C' => Some('\u{2102}'),
174            'H' => Some('\u{210D}'),
175            'N' => Some('\u{2115}'),
176            'P' => Some('\u{2119}'),
177            'Q' => Some('\u{211A}'),
178            'R' => Some('\u{211D}'),
179            'Z' => Some('\u{2124}'),
180            _ => match (cap, small) {
181                (Some(i), _) => cp(0x1D538, i),
182                (_, Some(i)) => cp(0x1D552, i),
183                _ => None,
184            },
185        },
186        // These 5 Unicode blocks (`primitives.cppo.ml`'s capitals/smalls
187        // folds) are gap-free — no exception chars, unlike
188        // Script/Fraktur/DoubleStruck above.
189        // KNOWN GAP: upstream also remaps DIGITS in every class (sans
190        // `0x1D7E2`, bold-sans `0x1D7EC`, typewriter `0x1D7F6`, …); this
191        // returns `None` for digits under every class.
192        MathCharClass::SansSerif => match (cap, small) {
193            (Some(i), _) => cp(0x1D5A0, i),
194            (_, Some(i)) => cp(0x1D5BA, i),
195            _ => None,
196        },
197        MathCharClass::BoldSansSerif => match (cap, small) {
198            (Some(i), _) => cp(0x1D5D4, i),
199            (_, Some(i)) => cp(0x1D5EE, i),
200            _ => None,
201        },
202        MathCharClass::ItalicSansSerif => match (cap, small) {
203            (Some(i), _) => cp(0x1D608, i),
204            (_, Some(i)) => cp(0x1D622, i),
205            _ => None,
206        },
207        MathCharClass::BoldItalicSansSerif => match (cap, small) {
208            (Some(i), _) => cp(0x1D63C, i),
209            (_, Some(i)) => cp(0x1D656, i),
210            _ => None,
211        },
212        MathCharClass::Typewriter => match (cap, small) {
213            (Some(i), _) => cp(0x1D670, i),
214            (_, Some(i)) => cp(0x1D68A, i),
215            _ => None,
216        },
217    }
218}