Skip to main content

math_core_renderer_internal/
attribute.rs

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