1use std::sync::LazyLock;
9
10use rustc_hash::FxHashMap as HashMap;
11
12fn make_plane1_map(
22 latin: u32,
23 greek_upper: Option<u32>,
24 greek_lower: Option<u32>,
25 digits: Option<u32>,
26) -> HashMap<char, char> {
27 let mut map = HashMap::default();
28 for i in 0..26u32 {
30 let src = char::from_u32('A' as u32 + i).unwrap();
31 let dst = char::from_u32(latin + i).unwrap();
32 map.insert(src, dst);
33 }
34 for i in 0..26u32 {
36 let src = char::from_u32('a' as u32 + i).unwrap();
37 let dst = char::from_u32(latin + 26 + i).unwrap();
38 map.insert(src, dst);
39 }
40 if let Some(base) = greek_upper {
42 for i in 0..25u32 {
43 let src = char::from_u32(0x0391 + i).unwrap();
44 let dst = char::from_u32(base + i).unwrap();
45 map.insert(src, dst);
46 }
47 }
48 if let Some(base) = greek_lower {
50 for i in 0..25u32 {
51 let src = char::from_u32(0x03B1 + i).unwrap();
52 let dst = char::from_u32(base + i).unwrap();
53 map.insert(src, dst);
54 }
55 }
56 if let Some(base) = digits {
58 for i in 0..10u32 {
59 let src = char::from_u32('0' as u32 + i).unwrap();
60 let dst = char::from_u32(base + i).unwrap();
61 map.insert(src, dst);
62 }
63 }
64 map
65}
66
67static UNICODE_MAP: LazyLock<HashMap<&'static str, HashMap<char, char>>> = LazyLock::new(|| {
71 let mut map: HashMap<&'static str, HashMap<char, char>> = HashMap::default();
72
73 map.insert(
75 "bold",
76 make_plane1_map(0x1D400, Some(0x1D6A8), Some(0x1D6C2), Some(0x1D7CE)),
77 );
78
79 {
81 let mut m = make_plane1_map(0x1D434, Some(0x1D6E2), Some(0x1D6FC), None);
82 m.insert('h', '\u{210E}');
83 map.insert("italic", m);
84 }
85
86 map.insert(
88 "bold-italic",
89 make_plane1_map(0x1D468, Some(0x1D71C), Some(0x1D736), None),
90 );
91
92 map.insert(
94 "sans-serif",
95 make_plane1_map(0x1D5A0, None, None, Some(0x1D7E2)),
96 );
97
98 map.insert(
100 "bold-sans-serif",
101 make_plane1_map(0x1D5D4, Some(0x1D756), Some(0x1D770), Some(0x1D7EC)),
102 );
103
104 map.insert(
106 "sans-serif-italic",
107 make_plane1_map(0x1D608, None, None, None),
108 );
109
110 map.insert(
112 "sans-serif-bold-italic",
113 make_plane1_map(0x1D63C, Some(0x1D790), Some(0x1D7AA), None),
114 );
115
116 map.insert(
118 "monospace",
119 make_plane1_map(0x1D670, None, None, Some(0x1D7F6)),
120 );
121
122 {
124 let mut m = make_plane1_map(0x1D49C, None, None, None);
125 m.insert('B', '\u{212C}'); m.insert('E', '\u{2130}'); m.insert('F', '\u{2131}'); m.insert('H', '\u{210B}'); m.insert('I', '\u{2110}'); m.insert('L', '\u{2112}'); m.insert('M', '\u{2133}'); m.insert('R', '\u{211B}'); m.insert('e', '\u{212F}'); m.insert('g', '\u{210A}'); m.insert('o', '\u{2134}'); map.insert("script", m);
137 }
138
139 map.insert("bold-script", make_plane1_map(0x1D4D0, None, None, None));
141
142 {
144 let mut m = make_plane1_map(0x1D504, None, None, None);
145 m.insert('C', '\u{212D}'); m.insert('H', '\u{210C}'); m.insert('I', '\u{2111}'); m.insert('R', '\u{211C}'); m.insert('Z', '\u{2128}'); map.insert("fraktur", m);
151 }
152
153 map.insert("bold-fraktur", make_plane1_map(0x1D56C, None, None, None));
155
156 {
158 let mut m = make_plane1_map(0x1D538, None, None, Some(0x1D7D8));
159 m.insert('C', '\u{2102}'); m.insert('H', '\u{210D}'); m.insert('N', '\u{2115}'); m.insert('P', '\u{2119}'); m.insert('Q', '\u{211A}'); m.insert('R', '\u{211D}'); m.insert('Z', '\u{2124}'); map.insert("double-struck", m);
167 }
168
169 {
171 let mut m = HashMap::default();
172 m.insert('\u{2032}', '\''); m.insert('0', '\u{2070}');
174 m.insert('1', '\u{00B9}');
175 m.insert('2', '\u{00B2}');
176 m.insert('3', '\u{00B3}');
177 m.insert('4', '\u{2074}');
178 m.insert('5', '\u{2075}');
179 m.insert('6', '\u{2076}');
180 m.insert('7', '\u{2077}');
181 m.insert('8', '\u{2078}');
182 m.insert('9', '\u{2079}');
183 m.insert('+', '\u{207A}');
184 m.insert('-', '\u{207B}');
185 m.insert('=', '\u{207C}');
186 m.insert('(', '\u{207D}');
187 m.insert(')', '\u{207E}');
188 m.insert('n', '\u{207F}');
189 m.insert('i', '\u{2071}');
190 m.insert('V', '\u{2C7D}');
191 m.insert('h', '\u{02B0}');
192 m.insert('j', '\u{02B2}');
193 m.insert('r', '\u{02B3}');
194 m.insert('w', '\u{02B7}');
195 m.insert('y', '\u{02B8}');
196 m.insert('s', '\u{02E2}');
197 m.insert('x', '\u{02E3}');
198 m.insert('A', '\u{1D2C}');
199 m.insert('\u{00C6}', '\u{1D2D}'); m.insert('B', '\u{1D2E}');
201 m.insert('D', '\u{1D30}');
202 m.insert('E', '\u{1D31}');
203 m.insert('G', '\u{1D33}');
204 m.insert('H', '\u{1D34}');
205 m.insert('I', '\u{1D35}');
206 m.insert('J', '\u{1D36}');
207 m.insert('K', '\u{1D37}');
208 m.insert('L', '\u{1D38}');
209 m.insert('M', '\u{1D39}');
210 m.insert('N', '\u{1D3A}');
211 m.insert('O', '\u{1D3C}');
212 m.insert('P', '\u{1D3E}');
213 m.insert('R', '\u{1D3F}');
214 m.insert('T', '\u{1D40}');
215 m.insert('U', '\u{1D41}');
216 m.insert('W', '\u{1D42}');
217 m.insert('a', '\u{1D43}');
218 m.insert('\u{03B1}', '\u{1D45}'); m.insert('\u{00E6}', '\u{1D46}'); m.insert('b', '\u{1D47}');
221 m.insert('d', '\u{1D48}');
222 m.insert('e', '\u{1D49}');
223 m.insert('\u{03B5}', '\u{1D4B}'); m.insert('\u{03F5}', '\u{1D4B}'); m.insert('g', '\u{1D4D}');
226 m.insert('!', '\u{1D4E}');
227 m.insert('k', '\u{1D4F}');
228 m.insert('m', '\u{1D50}');
229 m.insert('o', '\u{1D52}');
230 m.insert('p', '\u{1D56}');
231 m.insert('t', '\u{1D57}');
232 m.insert('u', '\u{1D58}');
233 m.insert('v', '\u{1D5B}');
234 m.insert('\u{03B2}', '\u{1D5D}'); m.insert('\u{03B3}', '\u{1D5E}'); m.insert('\u{03B4}', '\u{1D5F}'); m.insert('\u{03C6}', '\u{1D60}'); m.insert('\u{03D5}', '\u{1D60}'); m.insert('\u{03BE}', '\u{1D61}'); m.insert('c', '\u{1D9C}');
241 m.insert('f', '\u{1DA0}');
242 m.insert('\u{03A6}', '\u{1DB2}'); m.insert('\u{03C5}', '\u{1DB7}'); m.insert('z', '\u{1DBB}');
245 m.insert('\u{03B8}', '\u{1DBF}'); map.insert("superscript", m);
247 }
248
249 {
251 let mut m = HashMap::default();
252 m.insert('0', '\u{2080}');
253 m.insert('1', '\u{2081}');
254 m.insert('2', '\u{2082}');
255 m.insert('3', '\u{2083}');
256 m.insert('4', '\u{2084}');
257 m.insert('5', '\u{2085}');
258 m.insert('6', '\u{2086}');
259 m.insert('7', '\u{2087}');
260 m.insert('8', '\u{2088}');
261 m.insert('9', '\u{2089}');
262 m.insert('+', '\u{208A}');
263 m.insert('-', '\u{208B}');
264 m.insert('=', '\u{208C}');
265 m.insert('(', '\u{208D}');
266 m.insert(')', '\u{208E}');
267 m.insert('a', '\u{2090}');
268 m.insert('e', '\u{2091}');
269 m.insert('o', '\u{2092}');
270 m.insert('x', '\u{2093}');
271 m.insert('h', '\u{2095}');
273 m.insert('k', '\u{2096}');
274 m.insert('l', '\u{2097}');
275 m.insert('m', '\u{2098}');
276 m.insert('n', '\u{2099}');
277 m.insert('p', '\u{209A}');
278 m.insert('s', '\u{209B}');
279 m.insert('t', '\u{209C}');
280 m.insert('j', '\u{2C7C}');
281 m.insert('i', '\u{1D62}');
282 m.insert('r', '\u{1D63}');
283 m.insert('u', '\u{1D64}');
284 m.insert('v', '\u{1D65}');
285 m.insert('\u{03B2}', '\u{1D66}'); m.insert('\u{03B3}', '\u{1D67}'); m.insert('\u{03C1}', '\u{1D68}'); m.insert('\u{03C6}', '\u{1D69}'); m.insert('\u{03D5}', '\u{1D69}'); m.insert('\u{03BE}', '\u{1D6A}'); map.insert("subscript", m);
292 }
293
294 map
295});
296
297pub fn unicode_convert(string: &str, style: &str) -> Option<String> {
302 let mapping = UNICODE_MAP.get(style)?;
303 let mut result = String::with_capacity(string.len() * 4);
304 for ch in string.chars() {
305 match mapping.get(&ch) {
306 Some(&mapped) => result.push(mapped),
307 None => return None, }
309 }
310 Some(result)
311}
312
313static MATHVARIANTS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
317 let mut m = HashMap::default();
318 m.insert("upright", "normal");
319 m.insert("serif", "normal");
320 m.insert("medium", "normal");
321 m.insert("bold", "bold");
322 m.insert("italic", "italic");
323 m.insert("medium italic", "italic");
324 m.insert("bold italic", "bold-italic");
325 m.insert("doublestruck", "double-struck");
326 m.insert("blackboard", "double-struck");
327 m.insert("blackboard bold", "double-struck");
328 m.insert("blackboard upright", "double-struck");
329 m.insert("blackboard bold upright", "double-struck");
330 m.insert("fraktur", "fraktur");
331 m.insert("fraktur italic", "fraktur");
332 m.insert("fraktur bold", "bold-fraktur");
333 m.insert("script", "script");
334 m.insert("script italic", "script");
335 m.insert("script bold", "bold-script");
336 m.insert("caligraphic", "script");
337 m.insert("caligraphic bold", "bold-script");
338 m.insert("sansserif", "sans-serif");
339 m.insert("sansserif bold", "bold-sans-serif");
340 m.insert("sansserif italic", "sans-serif-italic");
341 m.insert("sansserif bold italic", "sans-serif-bold-italic");
342 m.insert("typewriter", "monospace");
343 m.insert("typewriter bold", "monospace");
344 m.insert("typewriter italic", "monospace");
345 m.insert("typewriter bold italic", "monospace");
346 m
347});
348
349pub fn unicode_mathvariant(font: &str) -> &'static str {
355 let mut f = font.to_string();
356
357 f = f.replace("slanted", "italic");
359
360 if f != "serif" {
362 f = remove_non_prefixed(&f, "serif");
365 }
366
367 if !f.starts_with("upright") {
370 f = f.replace("upright", "");
371 } else if f.len() > "upright".len() {
372 let (prefix, rest) = f.split_at("upright".len());
374 f = format!("{}{}", prefix, rest.replace("upright", ""));
375 }
376
377 if !f.starts_with("medium") {
380 f = f.replace("medium", "");
381 } else if f.len() > "medium".len() {
382 let (prefix, rest) = f.split_at("medium".len());
383 f = format!("{}{}", prefix, rest.replace("medium", ""));
384 }
385
386 let f = f.trim();
388
389 if let Some(&variant) = MATHVARIANTS.get(f) {
390 return variant;
391 }
392 "normal"
393}
394
395fn remove_non_prefixed(s: &str, word: &str) -> String {
398 let mut result = String::with_capacity(s.len());
399 let mut i = 0;
400 let bytes = s.as_bytes();
401 let word_bytes = word.as_bytes();
402 while i < bytes.len() {
403 if i + word_bytes.len() <= bytes.len() && &bytes[i..i + word_bytes.len()] == word_bytes {
404 let preceded_by_word = if i > 0 {
406 let prev = bytes[i - 1];
407 prev.is_ascii_alphanumeric() || prev == b'_'
408 } else {
409 false
410 };
411 if !preceded_by_word {
412 i += word_bytes.len();
413 continue;
414 }
415 }
416 result.push(bytes[i] as char);
417 i += 1;
418 }
419 result
420}
421
422pub fn plane1_hackable(variant: &str) -> Option<&'static str> {
427 match variant {
428 "script" => Some("script"),
429 "bold-script" => Some("script"),
430 "fraktur" => Some("fraktur"),
431 "bold-fraktur" => Some("fraktur"),
432 "double-struck" => Some("double-struck"),
433 _ => None,
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_unicode_convert_bold() {
443 let result = unicode_convert("ABC", "bold");
445 assert_eq!(result, Some("\u{1D400}\u{1D401}\u{1D402}".to_string()));
446 }
447
448 #[test]
449 fn test_unicode_convert_bold_lowercase() {
450 let result = unicode_convert("abc", "bold");
451 assert_eq!(result, Some("\u{1D41A}\u{1D41B}\u{1D41C}".to_string()));
452 }
453
454 #[test]
455 fn test_unicode_convert_italic_h() {
456 let result = unicode_convert("h", "italic");
458 assert_eq!(result, Some("\u{210E}".to_string()));
459 }
460
461 #[test]
462 fn test_unicode_convert_script_special() {
463 let result = unicode_convert("B", "script");
465 assert_eq!(result, Some("\u{212C}".to_string()));
466 }
467
468 #[test]
469 fn test_unicode_convert_double_struck_special() {
470 let result = unicode_convert("R", "double-struck");
472 assert_eq!(result, Some("\u{211D}".to_string()));
473 }
474
475 #[test]
476 fn test_unicode_convert_fraktur_special() {
477 let result = unicode_convert("C", "fraktur");
478 assert_eq!(result, Some("\u{212D}".to_string()));
479 }
480
481 #[test]
482 fn test_unicode_convert_all_or_nothing() {
483 let result = unicode_convert("A1", "italic");
485 assert_eq!(result, None);
486 }
487
488 #[test]
489 fn test_unicode_convert_bold_digits() {
490 let result = unicode_convert("123", "bold");
492 assert_eq!(result, Some("\u{1D7CF}\u{1D7D0}\u{1D7D1}".to_string()));
493 }
494
495 #[test]
496 fn test_unicode_convert_bold_greek() {
497 let result = unicode_convert("\u{0391}", "bold");
499 assert_eq!(result, Some("\u{1D6A8}".to_string()));
500 }
501
502 #[test]
503 fn test_unicode_convert_empty_string() {
504 let result = unicode_convert("", "bold");
505 assert_eq!(result, Some(String::new()));
506 }
507
508 #[test]
509 fn test_unicode_convert_unknown_style() {
510 let result = unicode_convert("A", "nonexistent");
511 assert_eq!(result, None);
512 }
513
514 #[test]
515 fn test_unicode_mathvariant_basic() {
516 assert_eq!(unicode_mathvariant("italic"), "italic");
517 assert_eq!(unicode_mathvariant("bold"), "bold");
518 assert_eq!(unicode_mathvariant("bold italic"), "bold-italic");
519 assert_eq!(unicode_mathvariant("typewriter"), "monospace");
520 assert_eq!(unicode_mathvariant("caligraphic"), "script");
521 }
522
523 #[test]
524 fn test_unicode_mathvariant_normalization() {
525 assert_eq!(unicode_mathvariant("slanted"), "italic");
527 assert_eq!(unicode_mathvariant("medium italic"), "italic");
529 assert_eq!(unicode_mathvariant("serif"), "normal");
531 assert_eq!(unicode_mathvariant("unknown_font"), "normal");
533 }
534
535 #[test]
536 fn test_unicode_mathvariant_compound() {
537 assert_eq!(
538 unicode_mathvariant("sansserif bold italic"),
539 "sans-serif-bold-italic"
540 );
541 assert_eq!(unicode_mathvariant("blackboard bold"), "double-struck");
542 assert_eq!(unicode_mathvariant("fraktur bold"), "bold-fraktur");
543 }
544
545 #[test]
546 fn test_plane1_hackable() {
547 assert_eq!(plane1_hackable("script"), Some("script"));
548 assert_eq!(plane1_hackable("bold-script"), Some("script"));
549 assert_eq!(plane1_hackable("bold"), None);
550 assert_eq!(plane1_hackable("italic"), None);
551 }
552}