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#[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 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 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 #[strum(serialize = "0.1667em")]
156 ThreeMu,
157 #[strum(serialize = "0.2222em")]
159 FourMu,
160 #[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#[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 Italic,
233 Monospace,
235 SansSerif,
236 SansSerifBoldItalic,
237 SansSerifItalic,
238 ScriptChancery,
239 ScriptRoundhand,
240 }
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 unsafe { char::from_u32_unchecked(c as u32 + offset) }
249}
250
251impl TextTransform {
252 #[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 _ => 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 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 pub color: Option<(u8, u8, u8)>,
451 pub style: Option<Style>,
453 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 ('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}