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    /// 3/18 of an em/\quad
141    #[strum(serialize = "0.1667em")]
142    ThreeMu,
143    /// 4/18 of an em/\quad
144    #[strum(serialize = "0.2222em")]
145    FourMu,
146    /// 5/18 of an em/\quad
147    #[strum(serialize = "0.2778em")]
148    FiveMu,
149}
150
151bitflags! {
152    #[repr(transparent)]
153    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
154    #[cfg_attr(feature = "serde", derive(Serialize))]
155    pub struct Notation: u8 {
156        const HORIZONTAL = 1; // (not used at the moment)
157        const UP_DIAGONAL = 1 << 1;
158        const DOWN_DIAGONAL = 1 << 2;
159    }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(Serialize))]
164pub enum HtmlTextStyle {
165    Bold = 1,
166    Italic,
167    BoldItalic,
168    Emphasis,
169    Typewriter,
170    SmallCaps,
171    SansSerif,
172    Serif,
173    Strikethrough,
174    Underline,
175}
176
177#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoStaticStr)]
178#[cfg_attr(feature = "serde", derive(Serialize))]
179pub enum HtmlTextSize {
180    #[strum(serialize = "50%")]
181    Size50,
182    #[strum(serialize = "60%")]
183    Size60,
184    #[strum(serialize = "70%")]
185    Size70,
186    #[strum(serialize = "80%")]
187    Size80,
188    #[strum(serialize = "90%")]
189    Size90,
190    #[strum(serialize = "100%")]
191    Size100,
192    #[strum(serialize = "120%")]
193    Size120,
194    #[strum(serialize = "140%")]
195    Size140,
196    #[strum(serialize = "170%")]
197    Size170,
198    #[strum(serialize = "200%")]
199    Size200,
200    #[strum(serialize = "250%")]
201    Size250,
202}
203
204// Transform of unicode characters.
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206#[cfg_attr(feature = "serde", derive(Serialize))]
207pub enum TextTransform {
208    Bold = 1,
209    BoldFraktur,
210    BoldItalic,
211    BoldSansSerif,
212    BoldScript,
213    DoubleStruck,
214    Fraktur,
215    // Initial,
216    Italic,
217    // Looped,
218    Monospace,
219    SansSerif,
220    SansSerifBoldItalic,
221    SansSerifItalic,
222    ScriptChancery,
223    ScriptRoundhand,
224    // Stretched,
225    // Tailed,
226}
227
228#[inline]
229const fn add_offset(c: char, offset: u32) -> char {
230    debug_assert!(char::from_u32(c as u32 + offset).is_some());
231    // SAFETY: the offsets are such that the resulting char should be valid.
232    unsafe { char::from_u32_unchecked(c as u32 + offset) }
233}
234
235impl TextTransform {
236    /// If the string does not have length 1, it is passed through unchanged.
237    // FIXME maybe we should do better than that?
238    #[inline]
239    pub const fn transform(self, ts: SuperChar, is_upright: bool) -> SuperChar {
240        match ts.try_as_char() {
241            Some(c) => self.transform_char(c, is_upright),
242            None => ts,
243        }
244    }
245
246    #[allow(clippy::manual_is_ascii_check)]
247    pub const fn transform_char(self, c: char, is_upright: bool) -> SuperChar {
248        let tf = if is_upright && matches!(self, TextTransform::BoldItalic) {
249            TextTransform::Bold
250        } else {
251            self
252        };
253        let mapped = match tf {
254            TextTransform::BoldScript => match c {
255                'A'..='Z' => Some(add_offset(c, 0x1D48F)),
256                'a'..='z' => Some(add_offset(c, 0x1D489)),
257                _ => None,
258            },
259            TextTransform::BoldItalic => match c {
260                'A'..='Z' => Some(add_offset(c, 0x1D427)),
261                'a'..='z' => Some(add_offset(c, 0x1D421)),
262                'Α'..='Ω' => Some(add_offset(c, 0x1D38B)),
263                'α'..='ω' => Some(add_offset(c, 0x1D385)),
264                'ϴ' => Some('𝜭'),
265                '∇' => Some('𝜵'),
266                '∂' => Some('𝝏'),
267                'ϵ' => Some('𝝐'),
268                'ϑ' => Some('𝝑'),
269                'ϰ' => Some('𝝒'),
270                'ϕ' => Some('𝝓'),
271                'ϱ' => Some('𝝔'),
272                'ϖ' => Some('𝝕'),
273                _ => None,
274            },
275            TextTransform::Bold => match c {
276                'A'..='Z' => Some(add_offset(c, 0x1D3BF)),
277                'a'..='z' => Some(add_offset(c, 0x1D3B9)),
278                'Α'..='Ω' => Some(add_offset(c, 0x1D317)),
279                'α'..='ω' => Some(add_offset(c, 0x1D311)),
280                'Ϝ'..='ϝ' => Some(add_offset(c, 0x1D3EE)),
281                '0'..='9' => Some(add_offset(c, 0x1D79E)),
282                'ϴ' => Some('𝚹'),
283                '∇' => Some('𝛁'),
284                '∂' => Some('𝛛'),
285                'ϵ' => Some('𝛜'),
286                'ϑ' => Some('𝛝'),
287                'ϰ' => Some('𝛞'),
288                'ϕ' => Some('𝛟'),
289                'ϱ' => Some('𝛠'),
290                'ϖ' => Some('𝛡'),
291                _ => None,
292            },
293            TextTransform::Fraktur => match c {
294                'A'..='B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => Some(add_offset(c, 0x1D4C3)),
295                'H'..='I' => Some(add_offset(c, 0x20C4)),
296                'a'..='z' => Some(add_offset(c, 0x1D4BD)),
297                'C' => Some('ℭ'),
298                'R' => Some('ℜ'),
299                'Z' => Some('ℨ'),
300                _ => None,
301            },
302            TextTransform::ScriptChancery | TextTransform::ScriptRoundhand => match c {
303                'A' | 'C'..='D' | 'G' | 'J'..='K' | 'N'..='Q' | 'S'..='Z' => {
304                    Some(add_offset(c, 0x1D45B))
305                }
306                'E'..='F' => Some(add_offset(c, 0x20EB)),
307                'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => Some(add_offset(c, 0x1D455)),
308                'B' => Some('ℬ'),
309                'H' => Some('ℋ'),
310                'I' => Some('ℐ'),
311                'L' => Some('ℒ'),
312                'M' => Some('ℳ'),
313                'R' => Some('ℛ'),
314                'e' => Some('ℯ'),
315                'g' => Some('ℊ'),
316                'o' => Some('ℴ'),
317                _ => None,
318            },
319            TextTransform::Monospace => match c {
320                'A'..='Z' => Some(add_offset(c, 0x1D62F)),
321                'a'..='z' => Some(add_offset(c, 0x1D629)),
322                '0'..='9' => Some(add_offset(c, 0x1D7C6)),
323                _ => None,
324            },
325            TextTransform::SansSerif => match c {
326                'A'..='Z' => Some(add_offset(c, 0x1D55F)),
327                'a'..='z' => Some(add_offset(c, 0x1D559)),
328                '0'..='9' => Some(add_offset(c, 0x1D7B2)),
329                _ => None,
330            },
331            TextTransform::BoldFraktur => match c {
332                'A'..='Z' => Some(add_offset(c, 0x1D52B)),
333                'a'..='z' => Some(add_offset(c, 0x1D525)),
334                _ => None,
335            },
336            TextTransform::SansSerifBoldItalic => match c {
337                'A'..='Z' => Some(add_offset(c, 0x1D5FB)),
338                'a'..='z' => Some(add_offset(c, 0x1D5F5)),
339                'Α'..='Ω' => Some(add_offset(c, 0x1D3FF)),
340                'α'..='ω' => Some(add_offset(c, 0x1D3F9)),
341                'ϴ' => Some('𝞡'),
342                '∇' => Some('𝞩'),
343                '∂' => Some('𝟃'),
344                'ϵ' => Some('𝟄'),
345                'ϑ' => Some('𝟅'),
346                'ϰ' => Some('𝟆'),
347                'ϕ' => Some('𝟇'),
348                'ϱ' => Some('𝟈'),
349                'ϖ' => Some('𝟉'),
350                _ => None,
351            },
352            TextTransform::SansSerifItalic => match c {
353                'A'..='Z' => Some(add_offset(c, 0x1D5C7)),
354                'a'..='z' => Some(add_offset(c, 0x1D5C1)),
355                _ => None,
356            },
357            TextTransform::BoldSansSerif => match c {
358                'A'..='Z' => Some(add_offset(c, 0x1D593)),
359                'a'..='z' => Some(add_offset(c, 0x1D58D)),
360                'Α'..='Ω' => Some(add_offset(c, 0x1D3C5)),
361                'α'..='ω' => Some(add_offset(c, 0x1D3BF)),
362                '0'..='9' => Some(add_offset(c, 0x1D7BC)),
363                'ϴ' => Some('𝝧'),
364                '∇' => Some('𝝯'),
365                '∂' => Some('𝞉'),
366                'ϵ' => Some('𝞊'),
367                'ϑ' => Some('𝞋'),
368                'ϰ' => Some('𝞌'),
369                'ϕ' => Some('𝞍'),
370                'ϱ' => Some('𝞎'),
371                'ϖ' => Some('𝞏'),
372                _ => None,
373            },
374            TextTransform::DoubleStruck => match c {
375                'A'..='B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => Some(add_offset(c, 0x1D4F7)),
376                'P'..='Q' => Some(add_offset(c, 0x20C9)),
377                'a'..='z' => Some(add_offset(c, 0x1D4F1)),
378                '0'..='9' => Some(add_offset(c, 0x1D7A8)),
379                'C' => Some('ℂ'),
380                'H' => Some('ℍ'),
381                'N' => Some('ℕ'),
382                'R' => Some('ℝ'),
383                'Z' => Some('ℤ'),
384                'π' => Some('ℼ'),
385                'γ' => Some('ℽ'),
386                'Γ' => Some('ℾ'),
387                'Π' => Some('ℿ'),
388                '∑' => Some('⅀'),
389                // FIXME: add Arabic double-struck characters
390                _ => None,
391            },
392            TextTransform::Italic => match c {
393                'A'..='Z' => Some(add_offset(c, 0x1D3F3)),
394                'a'..='g' | 'i'..='z' => Some(add_offset(c, 0x1D3ED)),
395                'Α'..='Ω' => Some(add_offset(c, 0x1D351)),
396                'α'..='ω' => Some(add_offset(c, 0x1D34B)),
397                'h' => Some('ℎ'),
398                'ı' => Some('𝚤'),
399                'ȷ' => Some('𝚥'),
400                'ϴ' => Some('𝛳'),
401                '∇' => Some('𝛻'),
402                '∂' => Some('𝜕'),
403                'ϵ' => Some('𝜖'),
404                'ϑ' => Some('𝜗'),
405                'ϰ' => Some('𝜘'),
406                'ϕ' => Some('𝜙'),
407                'ϱ' => Some('𝜚'),
408                'ϖ' => Some('𝜛'),
409                _ => None,
410            },
411        };
412
413        match mapped {
414            Some(mapped_char) => match tf {
415                TextTransform::ScriptChancery => {
416                    SuperChar::from_char_with_vs(mapped_char, VariationSelector::Vs1)
417                }
418                TextTransform::ScriptRoundhand => {
419                    SuperChar::from_char_with_vs(mapped_char, VariationSelector::Vs2)
420                }
421                _ => SuperChar::from_char(mapped_char),
422            },
423            None => SuperChar::from_char(c),
424        }
425    }
426}
427
428#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
429#[cfg_attr(feature = "serde", derive(Serialize))]
430pub struct RowAttrs {
431    // `color: …;` CSS property
432    pub color: Option<(u8, u8, u8)>,
433    // `style` attribute
434    pub style: Option<Style>,
435    // `math-shift: compact;` CSS property
436    pub math_shift_compact: bool,
437}
438
439impl RowAttrs {
440    pub const DEFAULT: Self = Self {
441        color: None,
442        style: None,
443        math_shift_compact: false,
444    };
445}
446
447#[cfg(test)]
448mod tests {
449    use crate::super_char::{SuperChar, VariationSelector};
450
451    use super::TextTransform;
452
453    #[test]
454    fn transform_test() {
455        let problems: [(char, TextTransform, SuperChar); _] = [
456            ('G', TextTransform::BoldScript, '𝓖'.into()),
457            ('H', TextTransform::Italic, '𝐻'.into()),
458            ('X', TextTransform::Fraktur, '𝔛'.into()),
459            (
460                'S',
461                TextTransform::ScriptChancery,
462                SuperChar::from_char_with_vs('𝒮', VariationSelector::Vs1),
463            ),
464            (
465                'M',
466                TextTransform::ScriptRoundhand,
467                SuperChar::from_char_with_vs('ℳ', VariationSelector::Vs2),
468            ),
469            ('f', TextTransform::Bold, '𝐟'.into()),
470            ('g', TextTransform::Bold, '𝐠'.into()),
471            ('o', TextTransform::DoubleStruck, '𝕠'.into()),
472            ('D', TextTransform::Monospace, '𝙳'.into()),
473            ('x', TextTransform::Monospace, '𝚡'.into()),
474            ('2', TextTransform::Monospace, '𝟸'.into()),
475            ('U', TextTransform::SansSerif, '𝖴'.into()),
476            ('v', TextTransform::SansSerif, '𝗏'.into()),
477            ('4', TextTransform::SansSerif, '𝟦'.into()),
478            ('A', TextTransform::SansSerifBoldItalic, '𝘼'.into()),
479            ('a', TextTransform::SansSerifBoldItalic, '𝙖'.into()),
480            ('Α', TextTransform::SansSerifBoldItalic, '𝞐'.into()),
481            ('α', TextTransform::SansSerifBoldItalic, '𝞪'.into()),
482            ('A', TextTransform::SansSerifItalic, '𝘈'.into()),
483            ('a', TextTransform::SansSerifItalic, '𝘢'.into()),
484            ('J', TextTransform::BoldSansSerif, '𝗝'.into()),
485            ('r', TextTransform::BoldSansSerif, '𝗿'.into()),
486            ('Ξ', TextTransform::BoldSansSerif, '𝝣'.into()),
487            ('τ', TextTransform::BoldSansSerif, '𝞃'.into()),
488        ];
489        for (source, transform, target) in problems.into_iter() {
490            assert_eq!(
491                target,
492                transform.transform_char(source, false),
493                "executed: {:?}({})",
494                transform,
495                source
496            );
497        }
498    }
499}