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