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 BoldItalic,
145 Emphasis,
146 Typewriter,
147 SmallCaps,
148 SansSerif,
149 Serif,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq)]
154#[cfg_attr(feature = "serde", derive(Serialize))]
155pub enum TextTransform {
156 Bold = 1,
157 BoldFraktur,
158 BoldItalic,
159 BoldSansSerif,
160 BoldScript,
161 DoubleStruck,
162 Fraktur,
163 Italic,
165 Monospace,
167 SansSerif,
168 SansSerifBoldItalic,
169 SansSerifItalic,
170 ScriptChancery,
171 ScriptRoundhand,
172 }
175
176#[inline]
177const fn add_offset(c: char, offset: u32) -> char {
178 debug_assert!(char::from_u32(c as u32 + offset).is_some());
179 unsafe { char::from_u32_unchecked(c as u32 + offset) }
181}
182
183impl TextTransform {
184 #[allow(clippy::manual_is_ascii_check)]
185 pub const fn transform(&self, c: char, is_upright: bool) -> char {
186 let tf = if is_upright && matches!(self, TextTransform::BoldItalic) {
187 &TextTransform::Bold
188 } else {
189 self
190 };
191 match tf {
192 TextTransform::BoldScript => match c {
193 'A'..='Z' => add_offset(c, 0x1D48F),
194 'a'..='z' => add_offset(c, 0x1D489),
195 _ => c,
196 },
197 TextTransform::BoldItalic => match c {
198 'A'..='Z' => add_offset(c, 0x1D427),
199 'a'..='z' => add_offset(c, 0x1D421),
200 'Α'..='Ω' => add_offset(c, 0x1D38B),
201 'α'..='ω' => add_offset(c, 0x1D385),
202 'ϴ' => '𝜭',
203 '∇' => '𝜵',
204 '∂' => '𝝏',
205 'ϵ' => '𝝐',
206 'ϑ' => '𝝑',
207 'ϰ' => '𝝒',
208 'ϕ' => '𝝓',
209 'ϱ' => '𝝔',
210 'ϖ' => '𝝕',
211 _ => c,
212 },
213 TextTransform::Bold => match c {
214 'A'..='Z' => add_offset(c, 0x1D3BF),
215 'a'..='z' => add_offset(c, 0x1D3B9),
216 'Α'..='Ω' => add_offset(c, 0x1D317),
217 'α'..='ω' => add_offset(c, 0x1D311),
218 'Ϝ'..='ϝ' => add_offset(c, 0x1D3EE),
219 '0'..='9' => add_offset(c, 0x1D79E),
220 'ϴ' => '𝚹',
221 '∇' => '𝛁',
222 '∂' => '𝛛',
223 'ϵ' => '𝛜',
224 'ϑ' => '𝛝',
225 'ϰ' => '𝛞',
226 'ϕ' => '𝛟',
227 'ϱ' => '𝛠',
228 'ϖ' => '𝛡',
229 _ => c,
230 },
231 TextTransform::Fraktur => match c {
232 'A'..='B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => add_offset(c, 0x1D4C3),
233 'H'..='I' => add_offset(c, 0x20C4),
234 'a'..='z' => add_offset(c, 0x1D4BD),
235 'C' => 'ℭ',
236 'R' => 'ℜ',
237 'Z' => 'ℨ',
238 _ => c,
239 },
240 TextTransform::ScriptChancery | TextTransform::ScriptRoundhand => match c {
241 'A' | 'C'..='D' | 'G' | 'J'..='K' | 'N'..='Q' | 'S'..='Z' => add_offset(c, 0x1D45B),
242 'E'..='F' => add_offset(c, 0x20EB),
243 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => add_offset(c, 0x1D455),
244 'B' => 'ℬ',
245 'H' => 'ℋ',
246 'I' => 'ℐ',
247 'L' => 'ℒ',
248 'M' => 'ℳ',
249 'R' => 'ℛ',
250 'e' => 'ℯ',
251 'g' => 'ℊ',
252 'o' => 'ℴ',
253 _ => c,
254 },
255 TextTransform::Monospace => match c {
256 'A'..='Z' => add_offset(c, 0x1D62F),
257 'a'..='z' => add_offset(c, 0x1D629),
258 '0'..='9' => add_offset(c, 0x1D7C6),
259 _ => c,
260 },
261 TextTransform::SansSerif => match c {
262 'A'..='Z' => add_offset(c, 0x1D55F),
263 'a'..='z' => add_offset(c, 0x1D559),
264 '0'..='9' => add_offset(c, 0x1D7B2),
265 _ => c,
266 },
267 TextTransform::BoldFraktur => match c {
268 'A'..='Z' => add_offset(c, 0x1D52B),
269 'a'..='z' => add_offset(c, 0x1D525),
270 _ => c,
271 },
272 TextTransform::SansSerifBoldItalic => match c {
273 'A'..='Z' => add_offset(c, 0x1D5FB),
274 'a'..='z' => add_offset(c, 0x1D5F5),
275 'Α'..='Ω' => add_offset(c, 0x1D3FF),
276 'α'..='ω' => add_offset(c, 0x1D3F9),
277 'ϴ' => '𝞡',
278 '∇' => '𝞩',
279 '∂' => '𝟃',
280 'ϵ' => '𝟄',
281 'ϑ' => '𝟅',
282 'ϰ' => '𝟆',
283 'ϕ' => '𝟇',
284 'ϱ' => '𝟈',
285 'ϖ' => '𝟉',
286 _ => c,
287 },
288 TextTransform::SansSerifItalic => match c {
289 'A'..='Z' => add_offset(c, 0x1D5C7),
290 'a'..='z' => add_offset(c, 0x1D5C1),
291 _ => c,
292 },
293 TextTransform::BoldSansSerif => match c {
294 'A'..='Z' => add_offset(c, 0x1D593),
295 'a'..='z' => add_offset(c, 0x1D58D),
296 'Α'..='Ω' => add_offset(c, 0x1D3C5),
297 'α'..='ω' => add_offset(c, 0x1D3BF),
298 '0'..='9' => add_offset(c, 0x1D7BC),
299 'ϴ' => '𝝧',
300 '∇' => '𝝯',
301 '∂' => '𝞉',
302 'ϵ' => '𝞊',
303 'ϑ' => '𝞋',
304 'ϰ' => '𝞌',
305 'ϕ' => '𝞍',
306 'ϱ' => '𝞎',
307 'ϖ' => '𝞏',
308 _ => c,
309 },
310 TextTransform::DoubleStruck => match c {
311 'A'..='B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => add_offset(c, 0x1D4F7),
312 'P'..='Q' => add_offset(c, 0x20C9),
313 'a'..='z' => add_offset(c, 0x1D4F1),
314 '0'..='9' => add_offset(c, 0x1D7A8),
315 'C' => 'ℂ',
316 'H' => 'ℍ',
317 'N' => 'ℕ',
318 'R' => 'ℝ',
319 'Z' => 'ℤ',
320 _ => c,
321 },
322 TextTransform::Italic => match c {
323 'A'..='Z' => add_offset(c, 0x1D3F3),
324 'a'..='g' | 'i'..='z' => add_offset(c, 0x1D3ED),
325 'Α'..='Ω' => add_offset(c, 0x1D351),
326 'α'..='ω' => add_offset(c, 0x1D34B),
327 'h' => 'ℎ',
328 'ı' => '𝚤',
329 'ȷ' => '𝚥',
330 'ϴ' => '𝛳',
331 '∇' => '𝛻',
332 '∂' => '𝜕',
333 'ϵ' => '𝜖',
334 'ϑ' => '𝜗',
335 'ϰ' => '𝜘',
336 'ϕ' => '𝜙',
337 'ϱ' => '𝜚',
338 'ϖ' => '𝜛',
339 _ => c,
340 },
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::{MathVariant, TextTransform};
348
349 #[test]
350 fn transform_test() {
351 let problems = [
352 ('G', TextTransform::BoldScript, '𝓖'),
353 ('H', TextTransform::Italic, '𝐻'),
354 ('X', TextTransform::Fraktur, '𝔛'),
355 ('S', TextTransform::ScriptChancery, '𝒮'),
356 ('f', TextTransform::Bold, '𝐟'),
357 ('g', TextTransform::Bold, '𝐠'),
358 ('o', TextTransform::DoubleStruck, '𝕠'),
359 ('D', TextTransform::Monospace, '𝙳'),
360 ('x', TextTransform::Monospace, '𝚡'),
361 ('2', TextTransform::Monospace, '𝟸'),
362 ('U', TextTransform::SansSerif, '𝖴'),
363 ('v', TextTransform::SansSerif, '𝗏'),
364 ('4', TextTransform::SansSerif, '𝟦'),
365 ('A', TextTransform::SansSerifBoldItalic, '𝘼'),
366 ('a', TextTransform::SansSerifBoldItalic, '𝙖'),
367 ('Α', TextTransform::SansSerifBoldItalic, '𝞐'),
368 ('α', TextTransform::SansSerifBoldItalic, '𝞪'),
369 ('A', TextTransform::SansSerifItalic, '𝘈'),
370 ('a', TextTransform::SansSerifItalic, '𝘢'),
371 ('J', TextTransform::BoldSansSerif, '𝗝'),
372 ('r', TextTransform::BoldSansSerif, '𝗿'),
373 ('Ξ', TextTransform::BoldSansSerif, '𝝣'),
374 ('τ', TextTransform::BoldSansSerif, '𝞃'),
375 ];
376 for (source, transform, target) in problems.into_iter() {
377 assert_eq!(
378 target,
379 transform.transform(source, false),
380 "executed: {:?}({})",
381 transform,
382 source
383 );
384 }
385 }
386
387 #[test]
388 fn size_test() {
389 assert_eq!(
390 std::mem::size_of::<MathVariant>(),
391 std::mem::size_of::<TextTransform>()
392 );
393 assert_eq!(
394 std::mem::size_of::<Option<MathVariant>>(),
395 std::mem::size_of::<TextTransform>()
396 );
397 }
398}