Skip to main content

ratex_types/
math_style.rs

1use serde::{Deserialize, Serialize};
2
3/// TeX math styles, controlling sizes of sub-expressions.
4///
5/// In TeX, the four main styles are D (display), T (text), S (script),
6/// SS (scriptscript). Each has a "cramped" variant where superscripts
7/// are set lower.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
9pub enum MathStyle {
10    #[default]
11    Display,
12    DisplayCramped,
13    Text,
14    TextCramped,
15    Script,
16    ScriptCramped,
17    ScriptScript,
18    ScriptScriptCramped,
19}
20
21impl MathStyle {
22    // KaTeX Style.ts lookup tables (indexed by style ID 0..7):
23    //   fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]
24    //   fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]
25    //   sup     = [S, Sc, S, Sc, SS, SSc, SS, SSc]
26    //   sub     = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]
27    //   cramp   = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]
28    //   text    = [D, Dc, T, Tc, T, Tc, T, Tc]
29
30    /// Style for the numerator of a fraction (preserves crampedness).
31    pub fn numerator(self) -> Self {
32        match self {
33            Self::Display => Self::Text,
34            Self::DisplayCramped => Self::TextCramped,
35            Self::Text => Self::Script,
36            Self::TextCramped => Self::ScriptCramped,
37            Self::Script | Self::ScriptScript => Self::ScriptScript,
38            Self::ScriptCramped | Self::ScriptScriptCramped => Self::ScriptScriptCramped,
39        }
40    }
41
42    /// Style for the denominator of a fraction (always cramped).
43    pub fn denominator(self) -> Self {
44        match self {
45            Self::Display | Self::DisplayCramped => Self::TextCramped,
46            Self::Text | Self::TextCramped => Self::ScriptCramped,
47            Self::Script | Self::ScriptCramped | Self::ScriptScript | Self::ScriptScriptCramped => {
48                Self::ScriptScriptCramped
49            }
50        }
51    }
52
53    /// Style for superscripts (preserves crampedness).
54    pub fn superscript(self) -> Self {
55        match self {
56            Self::Display | Self::Text => Self::Script,
57            Self::DisplayCramped | Self::TextCramped => Self::ScriptCramped,
58            Self::Script | Self::ScriptScript => Self::ScriptScript,
59            Self::ScriptCramped | Self::ScriptScriptCramped => Self::ScriptScriptCramped,
60        }
61    }
62
63    /// Style for subscripts (always cramped).
64    pub fn subscript(self) -> Self {
65        match self {
66            Self::Display | Self::DisplayCramped | Self::Text | Self::TextCramped => {
67                Self::ScriptCramped
68            }
69            Self::Script
70            | Self::ScriptCramped
71            | Self::ScriptScript
72            | Self::ScriptScriptCramped => Self::ScriptScriptCramped,
73        }
74    }
75
76    /// The cramped version of this style.
77    pub fn cramped(self) -> Self {
78        match self {
79            Self::Display => Self::DisplayCramped,
80            Self::Text => Self::TextCramped,
81            Self::Script => Self::ScriptCramped,
82            Self::ScriptScript => Self::ScriptScriptCramped,
83            other => other,
84        }
85    }
86
87    /// Convert to the text-size equivalent (Script/ScriptScript → Text).
88    /// Used inside `\text{}` blocks.
89    pub fn text(self) -> Self {
90        match self {
91            Self::Display => Self::Display,
92            Self::DisplayCramped => Self::DisplayCramped,
93            Self::Text => Self::Text,
94            Self::TextCramped => Self::TextCramped,
95            Self::Script | Self::ScriptScript => Self::Text,
96            Self::ScriptCramped | Self::ScriptScriptCramped => Self::TextCramped,
97        }
98    }
99
100    pub fn is_display(self) -> bool {
101        matches!(self, Self::Display | Self::DisplayCramped)
102    }
103
104    pub fn is_cramped(self) -> bool {
105        matches!(
106            self,
107            Self::DisplayCramped
108                | Self::TextCramped
109                | Self::ScriptCramped
110                | Self::ScriptScriptCramped
111        )
112    }
113
114    /// True for Script/ScriptScript sizes — affects inter-atom spacing.
115    pub fn is_tight(self) -> bool {
116        matches!(
117            self,
118            Self::Script
119                | Self::ScriptCramped
120                | Self::ScriptScript
121                | Self::ScriptScriptCramped
122        )
123    }
124
125    /// Size index for looking up font metrics (0=text, 1=script, 2=scriptscript).
126    pub fn size_index(self) -> usize {
127        match self {
128            Self::Display | Self::DisplayCramped | Self::Text | Self::TextCramped => 0,
129            Self::Script | Self::ScriptCramped => 1,
130            Self::ScriptScript | Self::ScriptScriptCramped => 2,
131        }
132    }
133
134    /// Size multiplier relative to base font size (TeX rule).
135    pub fn size_multiplier(self) -> f64 {
136        match self {
137            Self::Display | Self::DisplayCramped | Self::Text | Self::TextCramped => 1.0,
138            Self::Script | Self::ScriptCramped => 0.7,
139            Self::ScriptScript | Self::ScriptScriptCramped => 0.5,
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use MathStyle::*;
148
149    // Exhaustive tests against KaTeX Style.ts lookup tables.
150
151    #[test]
152    fn test_numerator_all_styles() {
153        // KaTeX: fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]
154        assert_eq!(Display.numerator(), Text);
155        assert_eq!(DisplayCramped.numerator(), TextCramped);
156        assert_eq!(Text.numerator(), Script);
157        assert_eq!(TextCramped.numerator(), ScriptCramped);
158        assert_eq!(Script.numerator(), ScriptScript);
159        assert_eq!(ScriptCramped.numerator(), ScriptScriptCramped);
160        assert_eq!(ScriptScript.numerator(), ScriptScript);
161        assert_eq!(ScriptScriptCramped.numerator(), ScriptScriptCramped);
162    }
163
164    #[test]
165    fn test_denominator_all_styles() {
166        // KaTeX: fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]
167        assert_eq!(Display.denominator(), TextCramped);
168        assert_eq!(DisplayCramped.denominator(), TextCramped);
169        assert_eq!(Text.denominator(), ScriptCramped);
170        assert_eq!(TextCramped.denominator(), ScriptCramped);
171        assert_eq!(Script.denominator(), ScriptScriptCramped);
172        assert_eq!(ScriptCramped.denominator(), ScriptScriptCramped);
173        assert_eq!(ScriptScript.denominator(), ScriptScriptCramped);
174        assert_eq!(ScriptScriptCramped.denominator(), ScriptScriptCramped);
175    }
176
177    #[test]
178    fn test_superscript_all_styles() {
179        // KaTeX: sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]
180        assert_eq!(Display.superscript(), Script);
181        assert_eq!(DisplayCramped.superscript(), ScriptCramped);
182        assert_eq!(Text.superscript(), Script);
183        assert_eq!(TextCramped.superscript(), ScriptCramped);
184        assert_eq!(Script.superscript(), ScriptScript);
185        assert_eq!(ScriptCramped.superscript(), ScriptScriptCramped);
186        assert_eq!(ScriptScript.superscript(), ScriptScript);
187        assert_eq!(ScriptScriptCramped.superscript(), ScriptScriptCramped);
188    }
189
190    #[test]
191    fn test_subscript_all_styles() {
192        // KaTeX: sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]
193        assert_eq!(Display.subscript(), ScriptCramped);
194        assert_eq!(DisplayCramped.subscript(), ScriptCramped);
195        assert_eq!(Text.subscript(), ScriptCramped);
196        assert_eq!(TextCramped.subscript(), ScriptCramped);
197        assert_eq!(Script.subscript(), ScriptScriptCramped);
198        assert_eq!(ScriptCramped.subscript(), ScriptScriptCramped);
199        assert_eq!(ScriptScript.subscript(), ScriptScriptCramped);
200        assert_eq!(ScriptScriptCramped.subscript(), ScriptScriptCramped);
201    }
202
203    #[test]
204    fn test_cramped_all_styles() {
205        // KaTeX: cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]
206        assert_eq!(Display.cramped(), DisplayCramped);
207        assert_eq!(DisplayCramped.cramped(), DisplayCramped);
208        assert_eq!(Text.cramped(), TextCramped);
209        assert_eq!(TextCramped.cramped(), TextCramped);
210        assert_eq!(Script.cramped(), ScriptCramped);
211        assert_eq!(ScriptCramped.cramped(), ScriptCramped);
212        assert_eq!(ScriptScript.cramped(), ScriptScriptCramped);
213        assert_eq!(ScriptScriptCramped.cramped(), ScriptScriptCramped);
214    }
215
216    #[test]
217    fn test_text_all_styles() {
218        // KaTeX: text = [D, Dc, T, Tc, T, Tc, T, Tc]
219        assert_eq!(Display.text(), Display);
220        assert_eq!(DisplayCramped.text(), DisplayCramped);
221        assert_eq!(Text.text(), Text);
222        assert_eq!(TextCramped.text(), TextCramped);
223        assert_eq!(Script.text(), Text);
224        assert_eq!(ScriptCramped.text(), TextCramped);
225        assert_eq!(ScriptScript.text(), Text);
226        assert_eq!(ScriptScriptCramped.text(), TextCramped);
227    }
228
229    #[test]
230    fn test_is_display() {
231        assert!(Display.is_display());
232        assert!(DisplayCramped.is_display());
233        assert!(!Text.is_display());
234        assert!(!Script.is_display());
235    }
236
237    #[test]
238    fn test_is_tight() {
239        assert!(!Display.is_tight());
240        assert!(!Text.is_tight());
241        assert!(Script.is_tight());
242        assert!(ScriptCramped.is_tight());
243        assert!(ScriptScript.is_tight());
244        assert!(ScriptScriptCramped.is_tight());
245    }
246
247    #[test]
248    fn test_size_index() {
249        assert_eq!(Display.size_index(), 0);
250        assert_eq!(Text.size_index(), 0);
251        assert_eq!(Script.size_index(), 1);
252        assert_eq!(ScriptScript.size_index(), 2);
253    }
254
255    #[test]
256    fn test_size_multiplier() {
257        assert!((Display.size_multiplier() - 1.0).abs() < f64::EPSILON);
258        assert!((Text.size_multiplier() - 1.0).abs() < f64::EPSILON);
259        assert!((Script.size_multiplier() - 0.7).abs() < f64::EPSILON);
260        assert!((ScriptScript.size_multiplier() - 0.5).abs() < f64::EPSILON);
261    }
262}