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#[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 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 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 #[strum(serialize = "0.1667em")]
142 ThreeMu,
143 #[strum(serialize = "0.2222em")]
145 FourMu,
146 #[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; 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#[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 Italic,
217 Monospace,
219 SansSerif,
220 SansSerifBoldItalic,
221 SansSerifItalic,
222 ScriptChancery,
223 ScriptRoundhand,
224 }
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 unsafe { char::from_u32_unchecked(c as u32 + offset) }
233}
234
235impl TextTransform {
236 #[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 _ => 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 pub color: Option<(u8, u8, u8)>,
433 pub style: Option<Style>,
435 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}