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#[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 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 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 #[strum(serialize = "0.1667em")]
144 ThreeMu,
145 #[strum(serialize = "0.2222em")]
147 FourMu,
148 #[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#[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 Italic,
221 Monospace,
223 SansSerif,
224 SansSerifBoldItalic,
225 SansSerifItalic,
226 ScriptChancery,
227 ScriptRoundhand,
228 }
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 unsafe { char::from_u32_unchecked(c as u32 + offset) }
237}
238
239impl TextTransform {
240 #[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 _ => 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 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 pub color: Option<(u8, u8, u8)>,
439 pub style: Option<Style>,
441 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 ('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}