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")]
141 ThreeMu, #[strum(serialize = "0.2222em")]
143 FourMu, #[strum(serialize = "0.2778em")]
145 FiveMu, }
147
148bitflags! {
149 #[repr(transparent)]
150 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
151 #[cfg_attr(feature = "serde", derive(Serialize))]
152 pub struct Notation: u8 {
153 const HORIZONTAL = 1; const UP_DIAGONAL = 1 << 1;
155 const DOWN_DIAGONAL = 1 << 2;
156 }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(Serialize))]
161pub enum HtmlTextStyle {
162 Bold = 1,
163 Italic,
164 BoldItalic,
165 Emphasis,
166 Typewriter,
167 SmallCaps,
168 SansSerif,
169 Serif,
170 Strikethrough,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoStaticStr)]
174#[cfg_attr(feature = "serde", derive(Serialize))]
175pub enum HtmlTextSize {
176 #[strum(serialize = "50%")]
177 Size50,
178 #[strum(serialize = "60%")]
179 Size60,
180 #[strum(serialize = "70%")]
181 Size70,
182 #[strum(serialize = "80%")]
183 Size80,
184 #[strum(serialize = "90%")]
185 Size90,
186 #[strum(serialize = "100%")]
187 Size100,
188 #[strum(serialize = "120%")]
189 Size120,
190 #[strum(serialize = "140%")]
191 Size140,
192 #[strum(serialize = "170%")]
193 Size170,
194 #[strum(serialize = "200%")]
195 Size200,
196 #[strum(serialize = "250%")]
197 Size250,
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202#[cfg_attr(feature = "serde", derive(Serialize))]
203pub enum TextTransform {
204 Bold = 1,
205 BoldFraktur,
206 BoldItalic,
207 BoldSansSerif,
208 BoldScript,
209 DoubleStruck,
210 Fraktur,
211 Italic,
213 Monospace,
215 SansSerif,
216 SansSerifBoldItalic,
217 SansSerifItalic,
218 ScriptChancery,
219 ScriptRoundhand,
220 }
223
224#[inline]
225const fn add_offset(c: char, offset: u32) -> char {
226 debug_assert!(char::from_u32(c as u32 + offset).is_some());
227 unsafe { char::from_u32_unchecked(c as u32 + offset) }
229}
230
231impl TextTransform {
232 #[inline]
235 pub const fn transform(self, ts: SuperChar, is_upright: bool) -> SuperChar {
236 match ts.try_as_char() {
237 Some(c) => self.transform_char(c, is_upright),
238 None => ts,
239 }
240 }
241
242 #[allow(clippy::manual_is_ascii_check)]
243 pub const fn transform_char(self, c: char, is_upright: bool) -> SuperChar {
244 let tf = if is_upright && matches!(self, TextTransform::BoldItalic) {
245 TextTransform::Bold
246 } else {
247 self
248 };
249 let mapped = match tf {
250 TextTransform::BoldScript => match c {
251 'A'..='Z' => Some(add_offset(c, 0x1D48F)),
252 'a'..='z' => Some(add_offset(c, 0x1D489)),
253 _ => None,
254 },
255 TextTransform::BoldItalic => match c {
256 'A'..='Z' => Some(add_offset(c, 0x1D427)),
257 'a'..='z' => Some(add_offset(c, 0x1D421)),
258 'Α'..='Ω' => Some(add_offset(c, 0x1D38B)),
259 'α'..='ω' => Some(add_offset(c, 0x1D385)),
260 'ϴ' => Some('𝜭'),
261 '∇' => Some('𝜵'),
262 '∂' => Some('𝝏'),
263 'ϵ' => Some('𝝐'),
264 'ϑ' => Some('𝝑'),
265 'ϰ' => Some('𝝒'),
266 'ϕ' => Some('𝝓'),
267 'ϱ' => Some('𝝔'),
268 'ϖ' => Some('𝝕'),
269 _ => None,
270 },
271 TextTransform::Bold => match c {
272 'A'..='Z' => Some(add_offset(c, 0x1D3BF)),
273 'a'..='z' => Some(add_offset(c, 0x1D3B9)),
274 'Α'..='Ω' => Some(add_offset(c, 0x1D317)),
275 'α'..='ω' => Some(add_offset(c, 0x1D311)),
276 'Ϝ'..='ϝ' => Some(add_offset(c, 0x1D3EE)),
277 '0'..='9' => Some(add_offset(c, 0x1D79E)),
278 'ϴ' => Some('𝚹'),
279 '∇' => Some('𝛁'),
280 '∂' => Some('𝛛'),
281 'ϵ' => Some('𝛜'),
282 'ϑ' => Some('𝛝'),
283 'ϰ' => Some('𝛞'),
284 'ϕ' => Some('𝛟'),
285 'ϱ' => Some('𝛠'),
286 'ϖ' => Some('𝛡'),
287 _ => None,
288 },
289 TextTransform::Fraktur => match c {
290 'A'..='B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => Some(add_offset(c, 0x1D4C3)),
291 'H'..='I' => Some(add_offset(c, 0x20C4)),
292 'a'..='z' => Some(add_offset(c, 0x1D4BD)),
293 'C' => Some('ℭ'),
294 'R' => Some('ℜ'),
295 'Z' => Some('ℨ'),
296 _ => None,
297 },
298 TextTransform::ScriptChancery | TextTransform::ScriptRoundhand => match c {
299 'A' | 'C'..='D' | 'G' | 'J'..='K' | 'N'..='Q' | 'S'..='Z' => {
300 Some(add_offset(c, 0x1D45B))
301 }
302 'E'..='F' => Some(add_offset(c, 0x20EB)),
303 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => Some(add_offset(c, 0x1D455)),
304 'B' => Some('ℬ'),
305 'H' => Some('ℋ'),
306 'I' => Some('ℐ'),
307 'L' => Some('ℒ'),
308 'M' => Some('ℳ'),
309 'R' => Some('ℛ'),
310 'e' => Some('ℯ'),
311 'g' => Some('ℊ'),
312 'o' => Some('ℴ'),
313 _ => None,
314 },
315 TextTransform::Monospace => match c {
316 'A'..='Z' => Some(add_offset(c, 0x1D62F)),
317 'a'..='z' => Some(add_offset(c, 0x1D629)),
318 '0'..='9' => Some(add_offset(c, 0x1D7C6)),
319 _ => None,
320 },
321 TextTransform::SansSerif => match c {
322 'A'..='Z' => Some(add_offset(c, 0x1D55F)),
323 'a'..='z' => Some(add_offset(c, 0x1D559)),
324 '0'..='9' => Some(add_offset(c, 0x1D7B2)),
325 _ => None,
326 },
327 TextTransform::BoldFraktur => match c {
328 'A'..='Z' => Some(add_offset(c, 0x1D52B)),
329 'a'..='z' => Some(add_offset(c, 0x1D525)),
330 _ => None,
331 },
332 TextTransform::SansSerifBoldItalic => match c {
333 'A'..='Z' => Some(add_offset(c, 0x1D5FB)),
334 'a'..='z' => Some(add_offset(c, 0x1D5F5)),
335 'Α'..='Ω' => Some(add_offset(c, 0x1D3FF)),
336 'α'..='ω' => Some(add_offset(c, 0x1D3F9)),
337 'ϴ' => Some('𝞡'),
338 '∇' => Some('𝞩'),
339 '∂' => Some('𝟃'),
340 'ϵ' => Some('𝟄'),
341 'ϑ' => Some('𝟅'),
342 'ϰ' => Some('𝟆'),
343 'ϕ' => Some('𝟇'),
344 'ϱ' => Some('𝟈'),
345 'ϖ' => Some('𝟉'),
346 _ => None,
347 },
348 TextTransform::SansSerifItalic => match c {
349 'A'..='Z' => Some(add_offset(c, 0x1D5C7)),
350 'a'..='z' => Some(add_offset(c, 0x1D5C1)),
351 _ => None,
352 },
353 TextTransform::BoldSansSerif => match c {
354 'A'..='Z' => Some(add_offset(c, 0x1D593)),
355 'a'..='z' => Some(add_offset(c, 0x1D58D)),
356 'Α'..='Ω' => Some(add_offset(c, 0x1D3C5)),
357 'α'..='ω' => Some(add_offset(c, 0x1D3BF)),
358 '0'..='9' => Some(add_offset(c, 0x1D7BC)),
359 'ϴ' => Some('𝝧'),
360 '∇' => Some('𝝯'),
361 '∂' => Some('𝞉'),
362 'ϵ' => Some('𝞊'),
363 'ϑ' => Some('𝞋'),
364 'ϰ' => Some('𝞌'),
365 'ϕ' => Some('𝞍'),
366 'ϱ' => Some('𝞎'),
367 'ϖ' => Some('𝞏'),
368 _ => None,
369 },
370 TextTransform::DoubleStruck => match c {
371 'A'..='B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => Some(add_offset(c, 0x1D4F7)),
372 'P'..='Q' => Some(add_offset(c, 0x20C9)),
373 'a'..='z' => Some(add_offset(c, 0x1D4F1)),
374 '0'..='9' => Some(add_offset(c, 0x1D7A8)),
375 'C' => Some('ℂ'),
376 'H' => Some('ℍ'),
377 'N' => Some('ℕ'),
378 'R' => Some('ℝ'),
379 'Z' => Some('ℤ'),
380 'π' => Some('ℼ'),
381 'γ' => Some('ℽ'),
382 'Γ' => Some('ℾ'),
383 'Π' => Some('ℿ'),
384 '∑' => Some('⅀'),
385 _ => None,
387 },
388 TextTransform::Italic => match c {
389 'A'..='Z' => Some(add_offset(c, 0x1D3F3)),
390 'a'..='g' | 'i'..='z' => Some(add_offset(c, 0x1D3ED)),
391 'Α'..='Ω' => Some(add_offset(c, 0x1D351)),
392 'α'..='ω' => Some(add_offset(c, 0x1D34B)),
393 'h' => Some('ℎ'),
394 'ı' => Some('𝚤'),
395 'ȷ' => Some('𝚥'),
396 'ϴ' => Some('𝛳'),
397 '∇' => Some('𝛻'),
398 '∂' => Some('𝜕'),
399 'ϵ' => Some('𝜖'),
400 'ϑ' => Some('𝜗'),
401 'ϰ' => Some('𝜘'),
402 'ϕ' => Some('𝜙'),
403 'ϱ' => Some('𝜚'),
404 'ϖ' => Some('𝜛'),
405 _ => None,
406 },
407 };
408
409 match mapped {
410 Some(mapped_char) => match tf {
411 TextTransform::ScriptChancery => {
412 SuperChar::from_char_with_vs(mapped_char, VariationSelector::Vs1)
413 }
414 TextTransform::ScriptRoundhand => {
415 SuperChar::from_char_with_vs(mapped_char, VariationSelector::Vs2)
416 }
417 _ => SuperChar::from_char(mapped_char),
418 },
419 None => SuperChar::from_char(c),
420 }
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use crate::super_char::{SuperChar, VariationSelector};
427
428 use super::TextTransform;
429
430 #[test]
431 fn transform_test() {
432 let problems: [(char, TextTransform, SuperChar); _] = [
433 ('G', TextTransform::BoldScript, '𝓖'.into()),
434 ('H', TextTransform::Italic, '𝐻'.into()),
435 ('X', TextTransform::Fraktur, '𝔛'.into()),
436 (
437 'S',
438 TextTransform::ScriptChancery,
439 SuperChar::from_char_with_vs('𝒮', VariationSelector::Vs1),
440 ),
441 (
442 'M',
443 TextTransform::ScriptRoundhand,
444 SuperChar::from_char_with_vs('ℳ', VariationSelector::Vs2),
445 ),
446 ('f', TextTransform::Bold, '𝐟'.into()),
447 ('g', TextTransform::Bold, '𝐠'.into()),
448 ('o', TextTransform::DoubleStruck, '𝕠'.into()),
449 ('D', TextTransform::Monospace, '𝙳'.into()),
450 ('x', TextTransform::Monospace, '𝚡'.into()),
451 ('2', TextTransform::Monospace, '𝟸'.into()),
452 ('U', TextTransform::SansSerif, '𝖴'.into()),
453 ('v', TextTransform::SansSerif, '𝗏'.into()),
454 ('4', TextTransform::SansSerif, '𝟦'.into()),
455 ('A', TextTransform::SansSerifBoldItalic, '𝘼'.into()),
456 ('a', TextTransform::SansSerifBoldItalic, '𝙖'.into()),
457 ('Α', TextTransform::SansSerifBoldItalic, '𝞐'.into()),
458 ('α', TextTransform::SansSerifBoldItalic, '𝞪'.into()),
459 ('A', TextTransform::SansSerifItalic, '𝘈'.into()),
460 ('a', TextTransform::SansSerifItalic, '𝘢'.into()),
461 ('J', TextTransform::BoldSansSerif, '𝗝'.into()),
462 ('r', TextTransform::BoldSansSerif, '𝗿'.into()),
463 ('Ξ', TextTransform::BoldSansSerif, '𝝣'.into()),
464 ('τ', TextTransform::BoldSansSerif, '𝞃'.into()),
465 ];
466 for (source, transform, target) in problems.into_iter() {
467 assert_eq!(
468 target,
469 transform.transform_char(source, false),
470 "executed: {:?}({})",
471 transform,
472 source
473 );
474 }
475 }
476}