math_core_renderer_internal/
attribute.rs

1use bitflags::bitflags;
2#[cfg(feature = "serde")]
3use serde::Serialize;
4
5use strum_macros::IntoStaticStr;
6
7/// <mi> mathvariant attribute
8#[derive(Debug, Clone, Copy, PartialEq)]
9#[cfg_attr(feature = "serde", derive(Serialize))]
10pub enum MathVariant {
11    /// This is enforced by setting `mathvariant="normal"`.
12    Normal,
13    /// This is enforced by transforming the characters themselves.
14    Transform(TextTransform),
15}
16
17impl MathVariant {
18    /// Returns `true` if the transformation is sensitive to whether a letter is "upright".
19    /// An example of an upright letter is "\Alpha".
20    #[inline]
21    pub fn differs_on_upright_letters(&self) -> bool {
22        matches!(self, MathVariant::Transform(TextTransform::BoldItalic))
23    }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
27#[cfg_attr(feature = "serde", derive(Serialize))]
28pub enum OpAttr {
29    #[strum(serialize = r#" stretchy="false""#)]
30    StretchyFalse = 1,
31    #[strum(serialize = r#" stretchy="true""#)]
32    StretchyTrue,
33    #[strum(serialize = r#" movablelimits="false""#)]
34    NoMovableLimits,
35    #[strum(serialize = r#" movablelimits="true""#)]
36    ForceMovableLimits,
37    #[strum(serialize = r#" form="prefix""#)]
38    FormPrefix,
39    #[strum(serialize = r#" form="postfix""#)]
40    FormPostfix,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44#[cfg_attr(feature = "serde", derive(Serialize))]
45pub enum LetterAttr {
46    Default,
47    ForcedUpright,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
51#[cfg_attr(feature = "serde", derive(Serialize))]
52pub enum Size {
53    #[strum(serialize = "1.2em")]
54    Scale1,
55    #[strum(serialize = "1.623em")]
56    Scale2,
57    #[strum(serialize = "2.047em")]
58    Scale3,
59    #[strum(serialize = "2.470em")]
60    Scale4,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(Serialize))]
65pub enum StretchMode {
66    /// Don't stretch the operator.
67    NoStretch = 1,
68    /// Operator is in a fence and should stretch.
69    Fence,
70    /// Operator is in the middle of a fenced expression and should stretch.
71    Middle,
72}
73
74/// display style
75#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
76#[cfg_attr(feature = "serde", derive(Serialize))]
77pub enum FracAttr {
78    #[strum(serialize = r#" displaystyle="true""#)]
79    DisplayStyleTrue = 1,
80    #[strum(serialize = r#" displaystyle="false""#)]
81    DisplayStyleFalse,
82    #[strum(serialize = r#" displaystyle="true" scriptlevel="0" style="padding-top: 0.1667em""#)]
83    CFracStyle,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
87#[cfg_attr(feature = "serde", derive(Serialize))]
88pub enum Style {
89    #[strum(serialize = r#" displaystyle="true" scriptlevel="0""#)]
90    Display = 1,
91    #[strum(serialize = r#" displaystyle="false" scriptlevel="0""#)]
92    Text,
93    #[strum(serialize = r#" displaystyle="false" scriptlevel="1""#)]
94    Script,
95    #[strum(serialize = r#" displaystyle="false" scriptlevel="2""#)]
96    ScriptScript,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
100#[cfg_attr(feature = "serde", derive(Serialize))]
101pub enum MathSpacing {
102    #[strum(serialize = "0")]
103    Zero = 1,
104    #[strum(serialize = "0.1667em")]
105    ThreeMu, // 3/18 of an em/\quad
106    #[strum(serialize = "0.2222em")]
107    FourMu, // 4/18 of an em/\quad
108    #[strum(serialize = "0.2778em")]
109    FiveMu, // 5/18 of an em/\quad
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
113#[cfg_attr(feature = "serde", derive(Serialize))]
114pub enum RowAttr {
115    Style(Style),
116    Color(u8, u8, u8),
117}
118
119bitflags! {
120    #[repr(transparent)]
121    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
122    #[cfg_attr(feature = "serde", derive(Serialize))]
123    pub struct Notation: u8 {
124        const HORIZONTAL = 1;
125        const UP_DIAGONAL = 1 << 1;
126        const DOWN_DIAGONAL = 1 << 2;
127    }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq)]
131#[cfg_attr(feature = "serde", derive(Serialize))]
132pub enum HtmlTextStyle {
133    Bold = 1,
134    Italic,
135    Emphasis,
136    Typewriter,
137    SmallCaps,
138}
139
140// Transform of unicode characters.
141#[derive(Debug, Clone, Copy, PartialEq)]
142#[cfg_attr(feature = "serde", derive(Serialize))]
143pub enum TextTransform {
144    Bold = 1,
145    BoldFraktur,
146    BoldItalic,
147    BoldSansSerif,
148    BoldScript,
149    DoubleStruck,
150    Fraktur,
151    // Initial,
152    Italic,
153    // Looped,
154    Monospace,
155    SansSerif,
156    SansSerifBoldItalic,
157    SansSerifItalic,
158    ScriptChancery,
159    ScriptRoundhand,
160    // Stretched,
161    // Tailed,
162}
163
164#[inline]
165const fn add_offset(c: char, offset: u32) -> char {
166    debug_assert!(char::from_u32(c as u32 + offset).is_some());
167    // SAFETY: the offsets are such that the resulting char should be valid.
168    unsafe { char::from_u32_unchecked(c as u32 + offset) }
169}
170
171impl TextTransform {
172    #[allow(clippy::manual_is_ascii_check)]
173    pub const fn transform(&self, c: char, is_upright: bool) -> char {
174        let tf = if is_upright && matches!(self, TextTransform::BoldItalic) {
175            &TextTransform::Bold
176        } else {
177            self
178        };
179        match tf {
180            TextTransform::BoldScript => match c {
181                'A'..='Z' => add_offset(c, 0x1D48F),
182                'a'..='z' => add_offset(c, 0x1D489),
183                _ => c,
184            },
185            TextTransform::BoldItalic => match c {
186                'A'..='Z' => add_offset(c, 0x1D427),
187                'a'..='z' => add_offset(c, 0x1D421),
188                'Α'..='Ω' => add_offset(c, 0x1D38B),
189                'α'..='ω' => add_offset(c, 0x1D385),
190                'ϴ' => '𝜭',
191                '∇' => '𝜵',
192                '∂' => '𝝏',
193                'ϵ' => '𝝐',
194                'ϑ' => '𝝑',
195                'ϰ' => '𝝒',
196                'ϕ' => '𝝓',
197                'ϱ' => '𝝔',
198                'ϖ' => '𝝕',
199                _ => c,
200            },
201            TextTransform::Bold => match c {
202                'A'..='Z' => add_offset(c, 0x1D3BF),
203                'a'..='z' => add_offset(c, 0x1D3B9),
204                'Α'..='Ω' => add_offset(c, 0x1D317),
205                'α'..='ω' => add_offset(c, 0x1D311),
206                'Ϝ'..='ϝ' => add_offset(c, 0x1D3EE),
207                '0'..='9' => add_offset(c, 0x1D79E),
208                'ϴ' => '𝚹',
209                '∇' => '𝛁',
210                '∂' => '𝛛',
211                'ϵ' => '𝛜',
212                'ϑ' => '𝛝',
213                'ϰ' => '𝛞',
214                'ϕ' => '𝛟',
215                'ϱ' => '𝛠',
216                'ϖ' => '𝛡',
217                _ => c,
218            },
219            TextTransform::Fraktur => match c {
220                'A'..='B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => add_offset(c, 0x1D4C3),
221                'H'..='I' => add_offset(c, 0x20C4),
222                'a'..='z' => add_offset(c, 0x1D4BD),
223                'C' => 'ℭ',
224                'R' => 'ℜ',
225                'Z' => 'ℨ',
226                _ => c,
227            },
228            TextTransform::ScriptChancery | TextTransform::ScriptRoundhand => match c {
229                'A' | 'C'..='D' | 'G' | 'J'..='K' | 'N'..='Q' | 'S'..='Z' => add_offset(c, 0x1D45B),
230                'E'..='F' => add_offset(c, 0x20EB),
231                'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => add_offset(c, 0x1D455),
232                'B' => 'ℬ',
233                'H' => 'ℋ',
234                'I' => 'ℐ',
235                'L' => 'ℒ',
236                'M' => 'ℳ',
237                'R' => 'ℛ',
238                'e' => 'ℯ',
239                'g' => 'ℊ',
240                'o' => 'ℴ',
241                _ => c,
242            },
243            TextTransform::Monospace => match c {
244                'A'..='Z' => add_offset(c, 0x1D62F),
245                'a'..='z' => add_offset(c, 0x1D629),
246                '0'..='9' => add_offset(c, 0x1D7C6),
247                _ => c,
248            },
249            TextTransform::SansSerif => match c {
250                'A'..='Z' => add_offset(c, 0x1D55F),
251                'a'..='z' => add_offset(c, 0x1D559),
252                '0'..='9' => add_offset(c, 0x1D7B2),
253                _ => c,
254            },
255            TextTransform::BoldFraktur => match c {
256                'A'..='Z' => add_offset(c, 0x1D52B),
257                'a'..='z' => add_offset(c, 0x1D525),
258                _ => c,
259            },
260            TextTransform::SansSerifBoldItalic => match c {
261                'A'..='Z' => add_offset(c, 0x1D5FB),
262                'a'..='z' => add_offset(c, 0x1D5F5),
263                'Α'..='Ω' => add_offset(c, 0x1D3FF),
264                'α'..='ω' => add_offset(c, 0x1D3F9),
265                'ϴ' => '𝞡',
266                '∇' => '𝞩',
267                '∂' => '𝟃',
268                'ϵ' => '𝟄',
269                'ϑ' => '𝟅',
270                'ϰ' => '𝟆',
271                'ϕ' => '𝟇',
272                'ϱ' => '𝟈',
273                'ϖ' => '𝟉',
274                _ => c,
275            },
276            TextTransform::SansSerifItalic => match c {
277                'A'..='Z' => add_offset(c, 0x1D5C7),
278                'a'..='z' => add_offset(c, 0x1D5C1),
279                _ => c,
280            },
281            TextTransform::BoldSansSerif => match c {
282                'A'..='Z' => add_offset(c, 0x1D593),
283                'a'..='z' => add_offset(c, 0x1D58D),
284                'Α'..='Ω' => add_offset(c, 0x1D3C5),
285                'α'..='ω' => add_offset(c, 0x1D3BF),
286                '0'..='9' => add_offset(c, 0x1D7BC),
287                'ϴ' => '𝝧',
288                '∇' => '𝝯',
289                '∂' => '𝞉',
290                'ϵ' => '𝞊',
291                'ϑ' => '𝞋',
292                'ϰ' => '𝞌',
293                'ϕ' => '𝞍',
294                'ϱ' => '𝞎',
295                'ϖ' => '𝞏',
296                _ => c,
297            },
298            TextTransform::DoubleStruck => match c {
299                'A'..='B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => add_offset(c, 0x1D4F7),
300                'P'..='Q' => add_offset(c, 0x20C9),
301                'a'..='z' => add_offset(c, 0x1D4F1),
302                '0'..='9' => add_offset(c, 0x1D7A8),
303                'C' => 'ℂ',
304                'H' => 'ℍ',
305                'N' => 'ℕ',
306                'R' => 'ℝ',
307                'Z' => 'ℤ',
308                _ => c,
309            },
310            TextTransform::Italic => match c {
311                'A'..='Z' => add_offset(c, 0x1D3F3),
312                'a'..='g' | 'i'..='z' => add_offset(c, 0x1D3ED),
313                'Α'..='Ω' => add_offset(c, 0x1D351),
314                'α'..='ω' => add_offset(c, 0x1D34B),
315                'h' => 'ℎ',
316                'ı' => '𝚤',
317                'ȷ' => '𝚥',
318                'ϴ' => '𝛳',
319                '∇' => '𝛻',
320                '∂' => '𝜕',
321                'ϵ' => '𝜖',
322                'ϑ' => '𝜗',
323                'ϰ' => '𝜘',
324                'ϕ' => '𝜙',
325                'ϱ' => '𝜚',
326                'ϖ' => '𝜛',
327                _ => c,
328            },
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::{MathVariant, TextTransform};
336
337    #[test]
338    fn transform_test() {
339        let problems = [
340            ('G', TextTransform::BoldScript, '𝓖'),
341            ('H', TextTransform::Italic, '𝐻'),
342            ('X', TextTransform::Fraktur, '𝔛'),
343            ('S', TextTransform::ScriptChancery, '𝒮'),
344            ('f', TextTransform::Bold, '𝐟'),
345            ('g', TextTransform::Bold, '𝐠'),
346            ('o', TextTransform::DoubleStruck, '𝕠'),
347            ('D', TextTransform::Monospace, '𝙳'),
348            ('x', TextTransform::Monospace, '𝚡'),
349            ('2', TextTransform::Monospace, '𝟸'),
350            ('U', TextTransform::SansSerif, '𝖴'),
351            ('v', TextTransform::SansSerif, '𝗏'),
352            ('4', TextTransform::SansSerif, '𝟦'),
353            ('A', TextTransform::SansSerifBoldItalic, '𝘼'),
354            ('a', TextTransform::SansSerifBoldItalic, '𝙖'),
355            ('Α', TextTransform::SansSerifBoldItalic, '𝞐'),
356            ('α', TextTransform::SansSerifBoldItalic, '𝞪'),
357            ('A', TextTransform::SansSerifItalic, '𝘈'),
358            ('a', TextTransform::SansSerifItalic, '𝘢'),
359            ('J', TextTransform::BoldSansSerif, '𝗝'),
360            ('r', TextTransform::BoldSansSerif, '𝗿'),
361            ('Ξ', TextTransform::BoldSansSerif, '𝝣'),
362            ('τ', TextTransform::BoldSansSerif, '𝞃'),
363        ];
364        for (source, transform, target) in problems.into_iter() {
365            assert_eq!(
366                target,
367                transform.transform(source, false),
368                "executed: {:?}({})",
369                transform,
370                source
371            );
372        }
373    }
374
375    #[test]
376    fn size_test() {
377        assert_eq!(
378            std::mem::size_of::<MathVariant>(),
379            std::mem::size_of::<TextTransform>()
380        );
381        assert_eq!(
382            std::mem::size_of::<Option<MathVariant>>(),
383            std::mem::size_of::<TextTransform>()
384        );
385    }
386}