1use std::rc::Rc;
2
3use rustc_hash::FxHashMap as HashMap;
4
5use crate::{
6 common::{
7 arena::{self, SymHashMap},
8 error::Result,
9 store::Stored,
10 },
11 digested::Digested,
12 pin, state,
13 tbox::Tbox,
14 token::Token,
15};
16
17const MATH_CLASS_ROLE: [&str; 8] = ["", "BIGOP", "BINOP", "RELOP", "OPEN", "CLOSE", "PUNCT", ""];
18
19#[derive(Debug, Clone, Default)]
21pub struct MathCharProps {
22 pub role: Option<String>,
23 pub glyph: Option<char>,
24 pub meaning: Option<String>,
25 pub name: Option<String>,
26 pub stretchy: Option<String>,
27 pub need_scriptpos: bool,
28 pub need_mathstyle: bool,
29 pub scriptpos: Option<String>,
30 pub mathstyle: Option<String>,
31 pub reversion: Option<crate::tokens::Tokens>,
32 pub font: Option<crate::common::font::Font>,
33}
34
35impl MathCharProps {
36 pub fn resolve_style_props(&mut self) {
38 let in_display = state::lookup_bool("IN_MATH_DISPLAY")
39 || state::lookup_font()
40 .map(|f| f.get_mathstyle().map(|s| s.as_ref()) == Some("display"))
41 .unwrap_or(false);
42 if self.need_scriptpos {
43 self.scriptpos = Some(if in_display { "mid" } else { "post" }.to_string());
44 }
45 if self.need_mathstyle {
46 self.mathstyle = Some(if in_display { "display" } else { "text" }.to_string());
47 }
48 }
49
50 pub fn into_props_map(self) -> HashMap<&'static str, Stored> {
52 let mut props = HashMap::default();
53 if let Some(role) = self.role {
54 props.insert("role", Stored::String(arena::pin(role)));
55 }
56 if let Some(meaning) = self.meaning {
57 props.insert("meaning", Stored::String(arena::pin(meaning)));
58 }
59 if let Some(name) = self.name {
60 props.insert("name", Stored::String(arena::pin(name)));
61 }
62 if let Some(stretchy) = self.stretchy {
63 props.insert("stretchy", Stored::String(arena::pin(stretchy)));
64 }
65 if let Some(scriptpos) = self.scriptpos {
66 props.insert("scriptpos", Stored::String(arena::pin(scriptpos)));
67 }
68 if let Some(mathstyle) = self.mathstyle {
69 props.insert("mathstyle", Stored::String(arena::pin(mathstyle)));
70 }
71 props
72 }
73}
74
75pub fn unicode_math_properties(c: char) -> Option<MathCharProps> {
77 let (role, meaning, name, stretchy, need_sp, need_ms) = match c {
80 '0'..='9' => ("NUMBER", Some(c.to_string()), None, None, false, false),
82 '=' => ("RELOP", Some("equals".into()), None, None, false, false),
84 '+' => ("ADDOP", Some("plus".into()), None, None, false, false),
85 '-' => ("ADDOP", Some("minus".into()), None, None, false, false),
86 '*' => ("MULOP", Some("times".into()), None, None, false, false),
87 '/' => ("MULOP", Some("divide".into()), None, None, false, false),
88 '!' => (
89 "POSTFIX",
90 Some("factorial".into()),
91 None,
92 None,
93 false,
94 false,
95 ),
96 ',' => ("PUNCT", None, None, None, false, false),
97 '.' => ("PERIOD", None, None, None, false, false),
98 ';' => ("PUNCT", None, None, None, false, false),
99 ':' => ("METARELOP", None, Some("colon".into()), None, false, false),
100 '|' => ("VERTBAR", None, None, Some("false".into()), false, false),
101 '<' => ("RELOP", Some("less-than".into()), None, None, false, false),
102 '>' => (
103 "RELOP",
104 Some("greater-than".into()),
105 None,
106 None,
107 false,
108 false,
109 ),
110 '(' => ("OPEN", None, None, Some("false".into()), false, false),
111 ')' => ("CLOSE", None, None, Some("false".into()), false, false),
112 '[' => ("OPEN", None, None, Some("false".into()), false, false),
113 ']' => ("CLOSE", None, None, Some("false".into()), false, false),
114 '{' => ("OPEN", None, None, Some("false".into()), false, false),
115 '}' => ("CLOSE", None, None, Some("false".into()), false, false),
116 '&' => ("ADDOP", Some("and".into()), None, None, false, false),
117 '%' => ("POSTFIX", Some("percent".into()), None, None, false, false),
118 '$' => (
119 "OPERATOR",
120 Some("currency-dollar".into()),
121 None,
122 None,
123 false,
124 false,
125 ),
126 '?' => ("UNKNOWN", None, None, None, false, false),
127 '\\' => ("ADDOP", Some("set-minus".into()), None, None, false, false),
129 '\u{00AC}' => ("BIGOP", Some("not".into()), None, None, false, false), '\u{00B1}' => (
132 "ADDOP",
133 Some("plus-or-minus".into()),
134 None,
135 None,
136 false,
137 false,
138 ), '\u{00D7}' => ("MULOP", Some("times".into()), None, None, false, false), '\u{00F7}' => ("MULOP", Some("divide".into()), None, None, false, false), '\u{2020}' => ("MULOP", None, None, None, false, false), '\u{2021}' => ("MULOP", None, None, None, false, false), '\u{2032}' => ("SUPOP", None, None, None, false, false), '\u{2061}' => ("APPLYOP", None, Some(String::new()), None, false, false), '\u{2062}' => (
147 "MULOP",
148 Some("times".into()),
149 Some(String::new()),
150 None,
151 false,
152 false,
153 ), '\u{2063}' => ("PUNCT", None, Some(String::new()), None, false, false), '\u{2064}' => (
156 "ADDOP",
157 Some("plus".into()),
158 Some(String::new()),
159 None,
160 false,
161 false,
162 ), '\u{210F}' => (
164 "ID",
165 Some("Planck-constant-over-2-pi".into()),
166 None,
167 None,
168 false,
169 false,
170 ), '\u{2111}' => (
172 "OPFUNCTION",
173 Some("imaginary-part".into()),
174 None,
175 None,
176 false,
177 false,
178 ), '\u{2118}' => (
180 "OPFUNCTION",
181 Some("Weierstrass-p".into()),
182 None,
183 None,
184 false,
185 false,
186 ), '\u{211C}' => (
188 "OPFUNCTION",
189 Some("real-part".into()),
190 None,
191 None,
192 false,
193 false,
194 ), '\u{2190}' => ("ARROW", None, None, None, false, false), '\u{2191}' => ("ARROW", None, Some("uparrow".into()), None, false, false), '\u{2192}' => ("ARROW", None, None, None, false, false), '\u{2193}' => ("ARROW", None, Some("downarrow".into()), None, false, false), '\u{2194}' => ("METARELOP", None, None, None, false, false), '\u{2195}' => (
202 "ARROW",
203 None,
204 Some("updownarrow".into()),
205 None,
206 false,
207 false,
208 ), '\u{2196}' => ("ARROW", None, None, None, false, false), '\u{2197}' => ("ARROW", None, None, None, false, false), '\u{2198}' => ("ARROW", None, None, None, false, false), '\u{2199}' => ("ARROW", None, None, None, false, false), '\u{219D}' => ("ARROW", Some("leads-to".into()), None, None, false, false), '\u{21A6}' => ("ARROW", Some("maps-to".into()), None, None, false, false), '\u{21A9}' => ("ARROW", None, None, None, false, false), '\u{21AA}' => ("ARROW", None, None, None, false, false), '\u{21BC}' => ("ARROW", None, None, None, false, false), '\u{21BD}' => ("ARROW", None, None, None, false, false), '\u{21C0}' => ("ARROW", None, None, None, false, false), '\u{21C1}' => ("ARROW", None, None, None, false, false), '\u{21CC}' => ("METARELOP", None, None, None, false, false), '\u{21D0}' => ("ARROW", None, None, None, false, false), '\u{21D1}' => ("ARROW", None, Some("Uparrow".into()), None, false, false), '\u{21D2}' => ("ARROW", None, None, None, false, false), '\u{21D3}' => ("ARROW", None, Some("Downarrow".into()), None, false, false), '\u{21D4}' => ("METARELOP", Some("iff".into()), None, None, false, false), '\u{21D5}' => (
228 "ARROW",
229 None,
230 Some("Updownarrow".into()),
231 None,
232 false,
233 false,
234 ), '\u{2200}' => ("BIGOP", Some("for-all".into()), None, None, false, false), '\u{2202}' => (
238 "DIFFOP",
239 Some("partial-differential".into()),
240 None,
241 None,
242 false,
243 false,
244 ), '\u{2203}' => ("BIGOP", Some("exists".into()), None, None, false, false), '\u{2205}' => ("ID", Some("empty-set".into()), None, None, false, false), '\u{2207}' => ("OPERATOR", None, None, None, false, false), '\u{2208}' => ("RELOP", Some("element-of".into()), None, None, false, false), '\u{2209}' => (
250 "RELOP",
251 Some("not-element-of".into()),
252 None,
253 None,
254 false,
255 false,
256 ), '\u{220B}' => ("RELOP", Some("contains".into()), None, None, false, false), '\u{220F}' => ("SUMOP", Some("product".into()), None, None, true, true), '\u{2210}' => ("SUMOP", Some("coproduct".into()), None, None, true, true), '\u{2211}' => ("SUMOP", Some("sum".into()), None, None, true, true), '\u{2213}' => (
264 "ADDOP",
265 Some("minus-or-plus".into()),
266 None,
267 None,
268 false,
269 false,
270 ), '\u{2216}' => ("ADDOP", Some("set-minus".into()), None, None, false, false), '\u{2217}' => ("MULOP", Some("times".into()), None, None, false, false), '\u{2218}' => ("MULOP", Some("compose".into()), None, None, false, false), '\u{2219}' => ("MULOP", None, None, None, false, false), '\u{221A}' => (
276 "OPERATOR",
277 Some("square-root".into()),
278 None,
279 None,
280 false,
281 false,
282 ), '\u{221D}' => (
284 "RELOP",
285 Some("proportional-to".into()),
286 None,
287 None,
288 false,
289 false,
290 ), '\u{221E}' => ("ID", Some("infinity".into()), None, None, false, false), '\u{2223}' => ("VERTBAR", None, None, None, false, false), '\u{2225}' => (
294 "VERTBAR",
295 Some("parallel-to".into()),
296 Some("||".into()),
297 None,
298 false,
299 false,
300 ), '\u{2227}' => ("ADDOP", Some("and".into()), None, None, false, false), '\u{2228}' => ("ADDOP", Some("or".into()), None, None, false, false), '\u{2229}' => (
305 "ADDOP",
306 Some("intersection".into()),
307 None,
308 None,
309 false,
310 false,
311 ), '\u{222A}' => ("ADDOP", Some("union".into()), None, None, false, false), '\u{222B}' => ("INTOP", Some("integral".into()), None, None, false, true), '\u{222E}' => (
316 "INTOP",
317 Some("contour-integral".into()),
318 None,
319 None,
320 false,
321 true,
322 ), '\u{223C}' => ("RELOP", Some("similar-to".into()), None, None, false, false), '\u{2240}' => ("MULOP", None, None, None, false, false), '\u{2243}' => (
327 "RELOP",
328 Some("similar-to-or-equals".into()),
329 None,
330 None,
331 false,
332 false,
333 ), '\u{2245}' => (
335 "RELOP",
336 Some("approximately-equals".into()),
337 None,
338 None,
339 false,
340 false,
341 ), '\u{2248}' => (
343 "RELOP",
344 Some("approximately-equals".into()),
345 None,
346 None,
347 false,
348 false,
349 ), '\u{224D}' => (
351 "RELOP",
352 Some("asymptotically-equals".into()),
353 None,
354 None,
355 false,
356 false,
357 ), '\u{2250}' => (
359 "RELOP",
360 Some("approaches-limit".into()),
361 None,
362 None,
363 false,
364 false,
365 ), '\u{2260}' => ("RELOP", Some("not-equals".into()), None, None, false, false), '\u{2261}' => (
368 "RELOP",
369 Some("equivalent-to".into()),
370 None,
371 None,
372 false,
373 false,
374 ), '\u{2264}' => (
376 "RELOP",
377 Some("less-than-or-equals".into()),
378 None,
379 None,
380 false,
381 false,
382 ), '\u{2265}' => (
384 "RELOP",
385 Some("greater-than-or-equals".into()),
386 None,
387 None,
388 false,
389 false,
390 ), '\u{226A}' => (
392 "RELOP",
393 Some("much-less-than".into()),
394 None,
395 None,
396 false,
397 false,
398 ), '\u{226B}' => (
400 "RELOP",
401 Some("much-greater-than".into()),
402 None,
403 None,
404 false,
405 false,
406 ), '\u{227A}' => ("RELOP", Some("precedes".into()), None, None, false, false), '\u{227B}' => ("RELOP", Some("succeeds".into()), None, None, false, false), '\u{2282}' => ("RELOP", Some("subset-of".into()), None, None, false, false), '\u{2283}' => (
412 "RELOP",
413 Some("superset-of".into()),
414 None,
415 None,
416 false,
417 false,
418 ), '\u{2286}' => (
420 "RELOP",
421 Some("subset-of-or-equals".into()),
422 None,
423 None,
424 false,
425 false,
426 ), '\u{2287}' => (
428 "RELOP",
429 Some("superset-of-or-equals".into()),
430 None,
431 None,
432 false,
433 false,
434 ), '\u{228E}' => ("ADDOP", None, None, None, false, false), '\u{228F}' => (
437 "RELOP",
438 Some("square-image-of".into()),
439 None,
440 None,
441 false,
442 false,
443 ), '\u{2290}' => (
445 "RELOP",
446 Some("square-original-of".into()),
447 None,
448 None,
449 false,
450 false,
451 ), '\u{2291}' => (
453 "RELOP",
454 Some("square-image-of-or-equals".into()),
455 None,
456 None,
457 false,
458 false,
459 ), '\u{2292}' => (
461 "RELOP",
462 Some("square-original-of-or-equals".into()),
463 None,
464 None,
465 false,
466 false,
467 ), '\u{2293}' => (
469 "ADDOP",
470 Some("square-intersection".into()),
471 None,
472 None,
473 false,
474 false,
475 ), '\u{2294}' => (
477 "ADDOP",
478 Some("square-union".into()),
479 None,
480 None,
481 false,
482 false,
483 ), '\u{2295}' => ("ADDOP", Some("direct-sum".into()), None, None, false, false), '\u{2296}' => (
487 "ADDOP",
488 Some("symmetric-difference".into()),
489 None,
490 None,
491 false,
492 false,
493 ), '\u{2297}' => (
495 "MULOP",
496 Some("tensor-product".into()),
497 None,
498 None,
499 false,
500 false,
501 ), '\u{2298}' => ("MULOP", None, None, None, false, false), '\u{2299}' => (
504 "MULOP",
505 Some("direct-product".into()),
506 None,
507 None,
508 false,
509 false,
510 ), '\u{22A2}' => ("METARELOP", Some("proves".into()), None, None, false, false), '\u{22A3}' => (
514 "METARELOP",
515 Some("does-not-prove".into()),
516 None,
517 None,
518 false,
519 false,
520 ), '\u{22A4}' => ("ADDOP", Some("top".into()), None, None, false, false), '\u{22A5}' => ("ADDOP", Some("bottom".into()), None, None, false, false), '\u{22A7}' => ("RELOP", Some("models".into()), None, None, false, false), '\u{22B2}' => (
525 "ADDOP",
526 Some("subgroup-of".into()),
527 None,
528 None,
529 false,
530 false,
531 ), '\u{22B3}' => (
533 "ADDOP",
534 Some("contains-as-subgroup".into()),
535 None,
536 None,
537 false,
538 false,
539 ), '\u{22B4}' => (
541 "ADDOP",
542 Some("subgroup-of-or-equals".into()),
543 None,
544 None,
545 false,
546 false,
547 ), '\u{22B5}' => (
549 "ADDOP",
550 Some("contains-as-subgroup-or-equals".into()),
551 None,
552 None,
553 false,
554 false,
555 ), '\u{22C0}' => ("SUMOP", Some("and".into()), None, None, true, true), '\u{22C1}' => ("SUMOP", Some("or".into()), None, None, true, true), '\u{22C2}' => ("SUMOP", Some("intersection".into()), None, None, true, true), '\u{22C3}' => ("SUMOP", Some("union".into()), None, None, true, true), '\u{22C4}' => ("ADDOP", None, None, None, false, false), '\u{22C5}' => ("MULOP", None, None, None, false, false), '\u{22C6}' => ("MULOP", None, None, None, false, false), '\u{22C8}' => ("RELOP", None, None, None, false, false), '\u{22EF}' => ("ID", None, None, None, false, false), '\u{22F1}' => ("ID", None, None, None, false, false), '\u{2308}' => (
569 "OPEN",
570 None,
571 Some("lceil".into()),
572 Some("false".into()),
573 false,
574 false,
575 ), '\u{2309}' => (
577 "CLOSE",
578 None,
579 Some("rceil".into()),
580 Some("false".into()),
581 false,
582 false,
583 ), '\u{230A}' => (
585 "OPEN",
586 None,
587 Some("lfloor".into()),
588 Some("false".into()),
589 false,
590 false,
591 ), '\u{230B}' => (
593 "CLOSE",
594 None,
595 Some("rfloor".into()),
596 Some("false".into()),
597 false,
598 false,
599 ), '\u{2322}' => ("RELOP", None, None, None, false, false), '\u{2323}' => ("RELOP", None, None, None, false, false), '\u{25B3}' => ("ADDOP", None, None, None, false, false), '\u{25B7}' => ("ADDOP", None, None, None, false, false), '\u{25B9}' => ("ADDOP", None, None, None, false, false), '\u{25BD}' => ("ADDOP", None, None, None, false, false), '\u{25C1}' => ("ADDOP", None, None, None, false, false), '\u{25C3}' => ("ADDOP", None, None, None, false, false), '\u{25CB}' => ("MULOP", None, None, None, false, false), '\u{27C2}' => (
611 "RELOP",
612 Some("perpendicular-to".into()),
613 None,
614 None,
615 false,
616 false,
617 ), '\u{27E8}' => (
620 "OPEN",
621 None,
622 Some("langle".into()),
623 Some("false".into()),
624 false,
625 false,
626 ), '\u{27E9}' => (
628 "CLOSE",
629 None,
630 Some("rangle".into()),
631 Some("false".into()),
632 false,
633 false,
634 ), '\u{27EE}' => (
636 "OPEN",
637 None,
638 Some("lgroup".into()),
639 Some("false".into()),
640 false,
641 false,
642 ), '\u{27EF}' => (
644 "CLOSE",
645 None,
646 Some("rgroup".into()),
647 Some("false".into()),
648 false,
649 false,
650 ), '\u{27F5}' => ("ARROW", None, None, None, false, false), '\u{27F6}' => ("ARROW", None, None, None, false, false), '\u{27F7}' => ("METARELOP", None, None, None, false, false), '\u{27F8}' => ("ARROW", None, None, None, false, false), '\u{27F9}' => ("ARROW", None, None, None, false, false), '\u{27FA}' => ("METARELOP", None, None, None, false, false), '\u{27FC}' => ("ARROW", None, None, None, false, false), '\u{2A00}' => ("SUMOP", None, None, None, true, true), '\u{2A01}' => ("SUMOP", Some("direct-sum".into()), None, None, true, true), '\u{2A02}' => (
663 "SUMOP",
664 Some("tensor-product".into()),
665 None,
666 None,
667 true,
668 true,
669 ), '\u{2A04}' => (
671 "SUMOP",
672 Some("symmetric-difference".into()),
673 None,
674 None,
675 true,
676 true,
677 ), '\u{2A06}' => ("SUMOP", Some("square-union".into()), None, None, true, true), '\u{2A1D}' => ("RELOP", Some("join".into()), None, None, false, false), '\u{2AAF}' => (
681 "RELOP",
682 Some("precedes-or-equals".into()),
683 None,
684 None,
685 false,
686 false,
687 ), '\u{2AB0}' => (
689 "RELOP",
690 Some("succeeds-or-equals".into()),
691 None,
692 None,
693 false,
694 false,
695 ), '\u{FF0F}' => ("OPFUNCTION", Some("not".into()), None, None, false, false), _ => return None,
698 };
699 Some(MathCharProps {
700 role: Some(role.to_string()),
701 glyph: None,
702 meaning,
703 name,
704 stretchy,
705 need_scriptpos: need_sp,
706 need_mathstyle: need_ms,
707 scriptpos: None,
708 mathstyle: None,
709 reversion: None,
710 font: None,
711 })
712}
713
714pub fn decode_math_char(
717 mut n: u16,
718 reversion: Option<crate::tokens::Tokens>,
719) -> Result<MathCharProps> {
720 let class: u16 = n / (16 * 256);
721 n %= 16 * 256;
722 let mut fam: u16 = n / 256;
723
724 let curfam_val: i32 = state::with_value("fontfamily", |v| match v {
732 Some(Stored::Int(i)) => *i as i32,
733 Some(Stored::Number(n)) => n.0 as i32,
734 _ => -1,
735 });
736
737 if class == 7 && (0..=15).contains(&curfam_val) {
738 fam = curfam_val as u16;
739 }
740 n %= 256;
741
742 let curfont = state::lookup_font().unwrap();
743 let initfont = state::with_value("initial_math_font", |v| match v {
747 Some(Stored::Font(f)) => Rc::clone(f),
748 _ => Rc::clone(&curfont),
749 });
750
751 let mut use_current_font = false;
752 let mut maybe_rev = curfam_val >= 0 && fam != 1;
753 let mut fontdef_tok: Option<Token> = None;
754
755 if class == 7 && curfam_val < 0 && curfont != initfont {
756 use_current_font = true;
757 maybe_rev = true;
758 fontdef_tok = Some(crate::T_CS!("\\font"));
759 }
760
761 let mut downsize = 0;
762 if fontdef_tok.is_none() {
763 let extract_token = |key: &str| -> Option<Token> {
766 state::with_value(key, |v| match v {
767 Some(Stored::Token(t)) => Some(*t),
768 _ => None,
769 })
770 };
771 let style = curfont
772 .get_mathstyle()
773 .map(|s| s.to_string())
774 .unwrap_or_default();
775 let style_str = if style == "script" || style == "scriptscript" || style == "text" {
776 style.as_str()
777 } else {
778 "text"
779 };
780 if style_str == "text" {
781 fontdef_tok = extract_token(&crate::s!("textfont_{fam}"));
782 } else if style_str == "script" {
783 fontdef_tok = extract_token(&crate::s!("scriptfont_{fam}"));
784 if fontdef_tok.is_none() {
785 fontdef_tok = extract_token(&crate::s!("textfont_{fam}"));
786 if fontdef_tok.is_some() {
787 downsize = 1;
788 }
789 }
790 } else if style_str == "scriptscript" {
791 fontdef_tok = extract_token(&crate::s!("scriptscriptfont_{fam}"));
792 if fontdef_tok.is_none() {
793 fontdef_tok = extract_token(&crate::s!("scriptfont_{fam}"));
794 if fontdef_tok.is_some() {
795 downsize = 1;
796 } else {
797 fontdef_tok = extract_token(&crate::s!("textfont_{fam}"));
798 if fontdef_tok.is_some() {
799 downsize = 2;
800 }
801 }
802 }
803 }
804 }
805
806 let c = n as u8 as char;
807 if (class as usize) >= MATH_CLASS_ROLE.len() {
810 return Ok(MathCharProps::default());
811 }
812 let class_role = MATH_CLASS_ROLE[class as usize];
813
814 let mut f = (*curfont).clone();
815 if let Some(ftok) = &fontdef_tok {
816 if use_current_font {
817 } else {
819 let preserved_size = f.size;
823 state::with_font_info(ftok, |fontinfo| {
824 if let Some(Stored::Font(info)) = fontinfo.unwrap_or(None) {
825 f = f.merge_ref(info);
826 } else {
827 let d_tok_opt = state::with_value("\\lx@default@font", |v| match v {
829 Some(Stored::Token(t)) => Some(*t),
830 _ => None,
831 });
832 if let Some(d_tok) = d_tok_opt {
833 state::with_font_info(&d_tok, |d_info| {
834 if let Some(Stored::Font(d_f)) = d_info.unwrap_or(None) {
835 f = f.merge_ref(d_f);
836 }
837 });
838 }
839 }
840 });
841 f.size = preserved_size;
842 }
843 }
844
845 if downsize > 0 {
846 f.scripted = Some(true);
847 }
848 if downsize > 1 {
849 f.scripted = Some(true);
850 }
851
852 let d = f.relative_to(&curfont);
853
854 let glyph = if use_current_font {
855 if let Some(ref data) = curfont.encoding {
856 crate::common::font::decode(n as u8, Some(data.to_string()), false)
857 } else {
858 Some(c)
859 }
860 } else if let Some(ftok) = &fontdef_tok {
861 let mut encoding_opt: Option<String> = state::with_font_info(ftok, |fontinfo| {
866 if let Some(Stored::Font(info)) = fontinfo? {
867 Ok::<Option<String>, crate::common::error::Error>(
868 info.encoding.as_ref().map(|s| s.to_string()),
869 )
870 } else {
871 Ok(None)
872 }
873 })?;
874 if encoding_opt.is_none() {
885 let shared_key = state::with_value(
886 &crate::s!("font_shared_key_{}", ftok.with_str(ToString::to_string)),
887 |v| match v {
888 Some(Stored::String(s)) => arena::with(*s, |str| Some(str.to_string())),
889 _ => None,
890 },
891 );
892 if let Some(sk) = shared_key {
893 if let Some(name) = sk.strip_prefix("fontinfo_")
895 && let Some(props) = crate::common::font::decode_fontname(name, None, None)
896 {
897 encoding_opt = props.encoding.as_ref().map(|s| s.to_string());
898 }
899 }
900 }
901 if let Some(data) = encoding_opt {
902 crate::common::font::decode(n as u8, Some(data), false)
903 } else {
904 Some(c)
905 }
906 } else {
907 Some(c)
908 };
909
910 let glyph_char = glyph.unwrap_or(c);
911 let charinfo = unicode_math_properties(glyph_char);
912 let mut props = charinfo.clone().unwrap_or_default();
913 props.glyph = glyph;
914
915 let mut role = charinfo.as_ref().and_then(|info| info.role.clone());
916 if role.is_none() && !class_role.is_empty() {
917 role = Some(class_role.to_string());
918 }
919 if role.is_some() && props.role.is_none() {
920 props.role = role;
921 }
922
923 props.resolve_style_props();
924
925 let mut final_reversion = reversion;
926 if let Some(rev) = final_reversion.clone() {
927 let mut wrap = maybe_rev && !d.is_empty();
928 if state::with_value("LaTeX.pool_loaded", |v| v.is_some()) {
929 wrap = false;
930 }
931 if wrap && let Some(ftok) = fontdef_tok {
932 let mut new_rev = vec![crate::T_BEGIN!(), ftok];
933 new_rev.extend(rev.unlist());
934 new_rev.push(crate::T_END!());
935 final_reversion = Some(crate::tokens::Tokens::new(new_rev));
936 }
937 props.reversion = final_reversion;
938 }
939
940 props.font = Some(f);
941
942 Ok(props)
943}
944
945pub fn decode_math_char_for_stomach(mathcode: u16, meaning: Token) -> Result<Option<Digested>> {
949 let props = decode_math_char(mathcode, Some(crate::Tokens!(meaning)))?;
950
951 let glyph = match props.glyph {
952 Some(g) => g,
953 None => return Ok(None),
954 };
955
956 let mut properties = SymHashMap::default();
957 properties.insert("mode", Stored::String(pin!("math")));
958 if let Some(ref role) = props.role {
959 properties.insert("role", Stored::String(arena::pin(role)));
960 }
961 if let Some(ref m) = props.meaning {
962 properties.insert("meaning", Stored::String(arena::pin(m)));
963 }
964 if let Some(ref name) = props.name {
965 properties.insert("name", Stored::String(arena::pin(name)));
966 }
967 if let Some(ref stretchy) = props.stretchy {
968 properties.insert("stretchy", Stored::String(arena::pin(stretchy)));
969 }
970 if let Some(ref scriptpos) = props.scriptpos {
971 properties.insert("scriptpos", Stored::String(arena::pin(scriptpos)));
972 }
973 if let Some(ref mathstyle) = props.mathstyle {
974 properties.insert("mathstyle", Stored::String(arena::pin(mathstyle)));
975 }
976
977 let glyph_sym = arena::pin_char(glyph);
978
979 let font = props
980 .font
981 .map(|f| Rc::new(arena::with(glyph_sym, |s| f.specialize(s))));
982 #[cfg(feature = "token-locators")]
988 let origin_loc: Option<crate::common::locator::Locator> =
989 crate::token::get_token_origin(meaning.loc).map(|o| {
990 arena::with(o.source, |s| {
991 crate::common::locator::Locator::new(s, o.line, o.col, o.line, o.col)
992 })
993 });
994 #[cfg(not(feature = "token-locators"))]
995 let origin_loc: Option<crate::common::locator::Locator> = None;
996 Ok(Some(Digested::from(Tbox::new(
997 glyph_sym,
998 font,
999 origin_loc,
1000 props.reversion.unwrap_or_else(|| crate::Tokens!(meaning)),
1001 properties,
1002 ))))
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn math_class_role_table_has_expected_values() {
1011 assert_eq!(MATH_CLASS_ROLE[0], "");
1014 assert_eq!(MATH_CLASS_ROLE[1], "BIGOP");
1015 assert_eq!(MATH_CLASS_ROLE[2], "BINOP");
1016 assert_eq!(MATH_CLASS_ROLE[3], "RELOP");
1017 assert_eq!(MATH_CLASS_ROLE[4], "OPEN");
1018 assert_eq!(MATH_CLASS_ROLE[5], "CLOSE");
1019 assert_eq!(MATH_CLASS_ROLE[6], "PUNCT");
1020 assert_eq!(MATH_CLASS_ROLE[7], "");
1021 }
1022
1023 #[test]
1024 fn unicode_math_properties_digits() {
1025 let p = unicode_math_properties('5').expect("digits should resolve");
1026 assert_eq!(p.role.as_deref(), Some("NUMBER"));
1027 assert_eq!(p.meaning.as_deref(), Some("5"));
1028 }
1029
1030 #[test]
1031 fn unicode_math_properties_basic_relops() {
1032 let eq = unicode_math_properties('=').unwrap();
1033 assert_eq!(eq.role.as_deref(), Some("RELOP"));
1034 assert_eq!(eq.meaning.as_deref(), Some("equals"));
1035 let lt = unicode_math_properties('<').unwrap();
1036 assert_eq!(lt.role.as_deref(), Some("RELOP"));
1037 assert_eq!(lt.meaning.as_deref(), Some("less-than"));
1038 }
1039
1040 #[test]
1041 fn unicode_math_properties_basic_addops() {
1042 let plus = unicode_math_properties('+').unwrap();
1043 assert_eq!(plus.role.as_deref(), Some("ADDOP"));
1044 assert_eq!(plus.meaning.as_deref(), Some("plus"));
1045 let minus = unicode_math_properties('-').unwrap();
1046 assert_eq!(minus.role.as_deref(), Some("ADDOP"));
1047 assert_eq!(minus.meaning.as_deref(), Some("minus"));
1048 }
1049
1050 #[test]
1051 fn unicode_math_properties_openclose() {
1052 for (c, role) in [
1054 ('(', "OPEN"),
1055 (')', "CLOSE"),
1056 ('[', "OPEN"),
1057 (']', "CLOSE"),
1058 ('{', "OPEN"),
1059 ('}', "CLOSE"),
1060 ] {
1061 let p = unicode_math_properties(c).unwrap();
1062 assert_eq!(p.role.as_deref(), Some(role), "{c}");
1063 assert_eq!(p.stretchy.as_deref(), Some("false"), "{c} stretchy");
1064 }
1065 }
1066
1067 #[test]
1068 fn unicode_math_properties_punct() {
1069 let comma = unicode_math_properties(',').unwrap();
1070 assert_eq!(comma.role.as_deref(), Some("PUNCT"));
1071 let semi = unicode_math_properties(';').unwrap();
1072 assert_eq!(semi.role.as_deref(), Some("PUNCT"));
1073 }
1074
1075 #[test]
1076 fn into_props_map_only_includes_set_fields() {
1077 let p = MathCharProps {
1080 role: Some("RELOP".into()),
1081 meaning: Some("equals".into()),
1082 name: None,
1083 stretchy: None,
1084 need_scriptpos: false,
1085 need_mathstyle: false,
1086 scriptpos: None,
1087 mathstyle: None,
1088 reversion: None,
1089 font: None,
1090 glyph: None,
1091 };
1092 let m = p.into_props_map();
1093 assert_eq!(m.len(), 2, "only role and meaning should populate");
1094 assert!(m.contains_key("role"));
1095 assert!(m.contains_key("meaning"));
1096 assert!(!m.contains_key("name"));
1097 assert!(!m.contains_key("stretchy"));
1098 }
1099
1100 #[test]
1101 fn into_props_map_empty_when_all_none() {
1102 let p = MathCharProps::default();
1103 let m = p.into_props_map();
1104 assert_eq!(m.len(), 0);
1105 }
1106}