Skip to main content

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