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