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