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