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