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