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