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