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