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}