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#[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 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, #[strum(serialize = "0.2222em")]
131 FourMu, #[strum(serialize = "0.2778em")]
133 FiveMu, }
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; 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#[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 Italic,
181 Monospace,
183 SansSerif,
184 SansSerifBoldItalic,
185 SansSerifItalic,
186 ScriptChancery,
187 ScriptRoundhand,
188 }
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 unsafe { char::from_u32_unchecked(c as u32 + offset) }
197}
198
199impl TextTransform {
200 #[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 _ => 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}