1use bitflags::bitflags;
2#[cfg(feature = "serde")]
3use serde::Serialize;
4
5use strum_macros::IntoStaticStr;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9#[cfg_attr(feature = "serde", derive(Serialize))]
10pub enum MathVariant {
11 Normal,
13 Transform(TextTransform),
15}
16
17impl MathVariant {
18 #[inline]
21 pub fn differs_on_upright_letters(&self) -> bool {
22 matches!(self, MathVariant::Transform(TextTransform::BoldItalic))
23 }
24}
25
26bitflags! {
27 #[repr(transparent)]
28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29 #[cfg_attr(feature = "serde", derive(Serialize))]
30 pub struct OpAttrs: u8 {
31 const STRETCHY_FALSE = 1;
32 const STRETCHY_TRUE = 1 << 1;
33 const NO_MOVABLE_LIMITS = 1 << 2;
34 const FORCE_MOVABLE_LIMITS = 1 << 3;
35 const FORM_PREFIX = 1 << 4;
36 const FORM_POSTFIX = 1 << 6;
38 const SYMMETRIC_TRUE = 1 << 7;
39 }
40}
41
42impl OpAttrs {
43 pub fn write_to(self, s: &mut String) {
44 debug_assert!(
45 !(self.contains(OpAttrs::STRETCHY_FALSE) && self.contains(OpAttrs::STRETCHY_TRUE)),
46 "STRETCHY_FALSE and STRETCHY_TRUE cannot both be set"
47 );
48 debug_assert!(
49 !(self.contains(OpAttrs::NO_MOVABLE_LIMITS)
50 && self.contains(OpAttrs::FORCE_MOVABLE_LIMITS)),
51 "NO_MOVABLE_LIMITS and FORCE_MOVABLE_LIMITS cannot both be set"
52 );
53 debug_assert!(
54 !(self.contains(OpAttrs::FORM_PREFIX) && self.contains(OpAttrs::FORM_POSTFIX)),
55 "FORM_PREFIX and FORM_POSTFIX cannot both be set"
56 );
57 if self.contains(OpAttrs::STRETCHY_FALSE) {
58 s.push_str(r#" stretchy="false""#);
59 }
60 if self.contains(OpAttrs::STRETCHY_TRUE) {
61 s.push_str(r#" stretchy="true""#);
62 }
63 if self.contains(OpAttrs::NO_MOVABLE_LIMITS) {
64 s.push_str(r#" movablelimits="false""#);
65 }
66 if self.contains(OpAttrs::FORCE_MOVABLE_LIMITS) {
67 s.push_str(r#" movablelimits="true""#);
68 }
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 if self.contains(OpAttrs::SYMMETRIC_TRUE) {
76 s.push_str(r#" symmetric="true""#);
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
82#[cfg_attr(feature = "serde", derive(Serialize))]
83pub enum LetterAttr {
84 Default,
85 ForcedUpright,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
89#[cfg_attr(feature = "serde", derive(Serialize))]
90pub enum Size {
91 #[strum(serialize = "1.2em")]
92 Scale1,
93 #[strum(serialize = "1.623em")]
94 Scale2,
95 #[strum(serialize = "2.047em")]
96 Scale3,
97 #[strum(serialize = "2.470em")]
98 Scale4,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
102#[cfg_attr(feature = "serde", derive(Serialize))]
103pub enum ParenType {
104 #[strum(serialize = r#" form="prefix""#)]
105 Open = 1,
106 #[strum(serialize = r#" form="postfix""#)]
107 Close,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
112#[cfg_attr(feature = "serde", derive(Serialize))]
113pub enum FracAttr {
114 #[strum(serialize = r#" displaystyle="true""#)]
115 DisplayStyleTrue = 1,
116 #[strum(serialize = r#" displaystyle="false""#)]
117 DisplayStyleFalse,
118 #[strum(serialize = r#" displaystyle="true" scriptlevel="0" style="padding-top: 0.1667em""#)]
119 CFracStyle,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
123#[cfg_attr(feature = "serde", derive(Serialize))]
124pub enum Style {
125 #[strum(serialize = r#" displaystyle="true" scriptlevel="0""#)]
126 Display = 1,
127 #[strum(serialize = r#" displaystyle="false" scriptlevel="0""#)]
128 Text,
129 #[strum(serialize = r#" displaystyle="false" scriptlevel="1""#)]
130 Script,
131 #[strum(serialize = r#" displaystyle="false" scriptlevel="2""#)]
132 ScriptScript,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, 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
148#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr)]
149#[cfg_attr(feature = "serde", derive(Serialize))]
150pub enum RowAttr {
151 Style(Style),
152 Color(u8, u8, u8),
153}
154
155bitflags! {
156 #[repr(transparent)]
157 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
158 #[cfg_attr(feature = "serde", derive(Serialize))]
159 pub struct Notation: u8 {
160 const HORIZONTAL = 1;
161 const UP_DIAGONAL = 1 << 1;
162 const DOWN_DIAGONAL = 1 << 2;
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq)]
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}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
181#[cfg_attr(feature = "serde", derive(Serialize))]
182pub enum TextTransform {
183 Bold = 1,
184 BoldFraktur,
185 BoldItalic,
186 BoldSansSerif,
187 BoldScript,
188 DoubleStruck,
189 Fraktur,
190 Italic,
192 Monospace,
194 SansSerif,
195 SansSerifBoldItalic,
196 SansSerifItalic,
197 ScriptChancery,
198 ScriptRoundhand,
199 }
202
203#[inline]
204const fn add_offset(c: char, offset: u32) -> char {
205 debug_assert!(char::from_u32(c as u32 + offset).is_some());
206 unsafe { char::from_u32_unchecked(c as u32 + offset) }
208}
209
210impl TextTransform {
211 #[allow(clippy::manual_is_ascii_check)]
212 pub const fn transform(&self, c: char, is_upright: bool) -> char {
213 let tf = if is_upright && matches!(self, TextTransform::BoldItalic) {
214 &TextTransform::Bold
215 } else {
216 self
217 };
218 match tf {
219 TextTransform::BoldScript => match c {
220 'A'..='Z' => add_offset(c, 0x1D48F),
221 'a'..='z' => add_offset(c, 0x1D489),
222 _ => c,
223 },
224 TextTransform::BoldItalic => match c {
225 'A'..='Z' => add_offset(c, 0x1D427),
226 'a'..='z' => add_offset(c, 0x1D421),
227 'Α'..='Ω' => add_offset(c, 0x1D38B),
228 'α'..='ω' => add_offset(c, 0x1D385),
229 'ϴ' => '𝜭',
230 '∇' => '𝜵',
231 '∂' => '𝝏',
232 'ϵ' => '𝝐',
233 'ϑ' => '𝝑',
234 'ϰ' => '𝝒',
235 'ϕ' => '𝝓',
236 'ϱ' => '𝝔',
237 'ϖ' => '𝝕',
238 _ => c,
239 },
240 TextTransform::Bold => match c {
241 'A'..='Z' => add_offset(c, 0x1D3BF),
242 'a'..='z' => add_offset(c, 0x1D3B9),
243 'Α'..='Ω' => add_offset(c, 0x1D317),
244 'α'..='ω' => add_offset(c, 0x1D311),
245 'Ϝ'..='ϝ' => add_offset(c, 0x1D3EE),
246 '0'..='9' => add_offset(c, 0x1D79E),
247 'ϴ' => '𝚹',
248 '∇' => '𝛁',
249 '∂' => '𝛛',
250 'ϵ' => '𝛜',
251 'ϑ' => '𝛝',
252 'ϰ' => '𝛞',
253 'ϕ' => '𝛟',
254 'ϱ' => '𝛠',
255 'ϖ' => '𝛡',
256 _ => c,
257 },
258 TextTransform::Fraktur => match c {
259 'A'..='B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => add_offset(c, 0x1D4C3),
260 'H'..='I' => add_offset(c, 0x20C4),
261 'a'..='z' => add_offset(c, 0x1D4BD),
262 'C' => 'ℭ',
263 'R' => 'ℜ',
264 'Z' => 'ℨ',
265 _ => c,
266 },
267 TextTransform::ScriptChancery | TextTransform::ScriptRoundhand => match c {
268 'A' | 'C'..='D' | 'G' | 'J'..='K' | 'N'..='Q' | 'S'..='Z' => add_offset(c, 0x1D45B),
269 'E'..='F' => add_offset(c, 0x20EB),
270 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => add_offset(c, 0x1D455),
271 'B' => 'ℬ',
272 'H' => 'ℋ',
273 'I' => 'ℐ',
274 'L' => 'ℒ',
275 'M' => 'ℳ',
276 'R' => 'ℛ',
277 'e' => 'ℯ',
278 'g' => 'ℊ',
279 'o' => 'ℴ',
280 _ => c,
281 },
282 TextTransform::Monospace => match c {
283 'A'..='Z' => add_offset(c, 0x1D62F),
284 'a'..='z' => add_offset(c, 0x1D629),
285 '0'..='9' => add_offset(c, 0x1D7C6),
286 _ => c,
287 },
288 TextTransform::SansSerif => match c {
289 'A'..='Z' => add_offset(c, 0x1D55F),
290 'a'..='z' => add_offset(c, 0x1D559),
291 '0'..='9' => add_offset(c, 0x1D7B2),
292 _ => c,
293 },
294 TextTransform::BoldFraktur => match c {
295 'A'..='Z' => add_offset(c, 0x1D52B),
296 'a'..='z' => add_offset(c, 0x1D525),
297 _ => c,
298 },
299 TextTransform::SansSerifBoldItalic => match c {
300 'A'..='Z' => add_offset(c, 0x1D5FB),
301 'a'..='z' => add_offset(c, 0x1D5F5),
302 'Α'..='Ω' => add_offset(c, 0x1D3FF),
303 'α'..='ω' => add_offset(c, 0x1D3F9),
304 'ϴ' => '𝞡',
305 '∇' => '𝞩',
306 '∂' => '𝟃',
307 'ϵ' => '𝟄',
308 'ϑ' => '𝟅',
309 'ϰ' => '𝟆',
310 'ϕ' => '𝟇',
311 'ϱ' => '𝟈',
312 'ϖ' => '𝟉',
313 _ => c,
314 },
315 TextTransform::SansSerifItalic => match c {
316 'A'..='Z' => add_offset(c, 0x1D5C7),
317 'a'..='z' => add_offset(c, 0x1D5C1),
318 _ => c,
319 },
320 TextTransform::BoldSansSerif => match c {
321 'A'..='Z' => add_offset(c, 0x1D593),
322 'a'..='z' => add_offset(c, 0x1D58D),
323 'Α'..='Ω' => add_offset(c, 0x1D3C5),
324 'α'..='ω' => add_offset(c, 0x1D3BF),
325 '0'..='9' => add_offset(c, 0x1D7BC),
326 'ϴ' => '𝝧',
327 '∇' => '𝝯',
328 '∂' => '𝞉',
329 'ϵ' => '𝞊',
330 'ϑ' => '𝞋',
331 'ϰ' => '𝞌',
332 'ϕ' => '𝞍',
333 'ϱ' => '𝞎',
334 'ϖ' => '𝞏',
335 _ => c,
336 },
337 TextTransform::DoubleStruck => match c {
338 'A'..='B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => add_offset(c, 0x1D4F7),
339 'P'..='Q' => add_offset(c, 0x20C9),
340 'a'..='z' => add_offset(c, 0x1D4F1),
341 '0'..='9' => add_offset(c, 0x1D7A8),
342 'C' => 'ℂ',
343 'H' => 'ℍ',
344 'N' => 'ℕ',
345 'R' => 'ℝ',
346 'Z' => 'ℤ',
347 _ => c,
348 },
349 TextTransform::Italic => match c {
350 'A'..='Z' => add_offset(c, 0x1D3F3),
351 'a'..='g' | 'i'..='z' => add_offset(c, 0x1D3ED),
352 'Α'..='Ω' => add_offset(c, 0x1D351),
353 'α'..='ω' => add_offset(c, 0x1D34B),
354 'h' => 'ℎ',
355 'ı' => '𝚤',
356 'ȷ' => '𝚥',
357 'ϴ' => '𝛳',
358 '∇' => '𝛻',
359 '∂' => '𝜕',
360 'ϵ' => '𝜖',
361 'ϑ' => '𝜗',
362 'ϰ' => '𝜘',
363 'ϕ' => '𝜙',
364 'ϱ' => '𝜚',
365 'ϖ' => '𝜛',
366 _ => c,
367 },
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::{MathVariant, TextTransform};
375
376 #[test]
377 fn transform_test() {
378 let problems = [
379 ('G', TextTransform::BoldScript, '𝓖'),
380 ('H', TextTransform::Italic, '𝐻'),
381 ('X', TextTransform::Fraktur, '𝔛'),
382 ('S', TextTransform::ScriptChancery, '𝒮'),
383 ('f', TextTransform::Bold, '𝐟'),
384 ('g', TextTransform::Bold, '𝐠'),
385 ('o', TextTransform::DoubleStruck, '𝕠'),
386 ('D', TextTransform::Monospace, '𝙳'),
387 ('x', TextTransform::Monospace, '𝚡'),
388 ('2', TextTransform::Monospace, '𝟸'),
389 ('U', TextTransform::SansSerif, '𝖴'),
390 ('v', TextTransform::SansSerif, '𝗏'),
391 ('4', TextTransform::SansSerif, '𝟦'),
392 ('A', TextTransform::SansSerifBoldItalic, '𝘼'),
393 ('a', TextTransform::SansSerifBoldItalic, '𝙖'),
394 ('Α', TextTransform::SansSerifBoldItalic, '𝞐'),
395 ('α', TextTransform::SansSerifBoldItalic, '𝞪'),
396 ('A', TextTransform::SansSerifItalic, '𝘈'),
397 ('a', TextTransform::SansSerifItalic, '𝘢'),
398 ('J', TextTransform::BoldSansSerif, '𝗝'),
399 ('r', TextTransform::BoldSansSerif, '𝗿'),
400 ('Ξ', TextTransform::BoldSansSerif, '𝝣'),
401 ('τ', TextTransform::BoldSansSerif, '𝞃'),
402 ];
403 for (source, transform, target) in problems.into_iter() {
404 assert_eq!(
405 target,
406 transform.transform(source, false),
407 "executed: {:?}({})",
408 transform,
409 source
410 );
411 }
412 }
413
414 #[test]
415 fn size_test() {
416 assert_eq!(
417 std::mem::size_of::<MathVariant>(),
418 std::mem::size_of::<TextTransform>()
419 );
420 assert_eq!(
421 std::mem::size_of::<Option<MathVariant>>(),
422 std::mem::size_of::<TextTransform>()
423 );
424 }
425}