1use libxml::tree::Node;
16use rustc_hash::FxHashMap as HashMap;
17
18use crate::document::{NodeData, PostDocument, element_children};
19
20pub(crate) fn meaning_to_cmml_element_pub(meaning: &str) -> Option<&'static str> {
25 meaning_to_cmml_element(meaning)
26}
27
28fn meaning_to_cmml_element(meaning: &str) -> Option<&'static str> {
29 match meaning {
30 "plus" => Some("m:plus"),
32 "minus" | "uminus" => Some("m:minus"),
33 "times" => Some("m:times"),
34 "divide" => Some("m:divide"),
35 "power" => Some("m:power"),
36 "quotient" => Some("m:quotient"),
37 "factorial" => Some("m:factorial"),
38 "remainder" => Some("m:rem"),
39 "maximum" => Some("m:max"),
40 "minimum" => Some("m:min"),
41 "gcd" => Some("m:gcd"),
42 "lcm" => Some("m:lcm"),
43 "absolute-value" => Some("m:abs"),
44 "conjugate" => Some("m:conjugate"),
45 "argument" => Some("m:arg"),
46 "real-part" => Some("m:real"),
47 "imaginary-part" => Some("m:imaginary"),
48 "floor" => Some("m:floor"),
49 "ceiling" => Some("m:ceiling"),
50 "and" => Some("m:and"),
52 "or" => Some("m:or"),
53 "xor" => Some("m:xor"),
54 "not" => Some("m:not"),
55 "implies" => Some("m:implies"),
56 "forall" => Some("m:forall"),
57 "exists" => Some("m:exists"),
58 "equals" => Some("m:eq"),
60 "not-equals" => Some("m:neq"),
61 "greater-than" => Some("m:gt"),
62 "less-than" => Some("m:lt"),
63 "greater-than-or-equals" => Some("m:geq"),
64 "less-than-or-equals" => Some("m:leq"),
65 "equivalent-to" => Some("m:equivalent"),
66 "approximately-equals" => Some("m:approx"),
67 "factor-of" => Some("m:factorof"),
68 "integral" => Some("m:int"),
70 "differential" => Some("m:diff"),
71 "partial-differential" => Some("m:partialdiff"),
72 "divergence" => Some("m:divergence"),
73 "gradient" => Some("m:grad"),
74 "curl" => Some("m:curl"),
75 "laplacian" => Some("m:laplacian"),
76 "union" => Some("m:union"),
78 "intersection" => Some("m:intersect"),
79 "element-of" => Some("m:in"),
80 "not-element-of" => Some("m:notin"),
81 "subset-of" | "subset-of-or-equals" => Some("m:subset"),
82 "subset-of-and-not-equals" => Some("m:prsubset"),
83 "set-minus" => Some("m:setdiff"),
84 "cardinality" => Some("m:card"),
85 "cartesian-product" => Some("m:cartesianproduct"),
86 "sum" => Some("m:sum"),
88 "prod" => Some("m:prod"),
89 "limit" => Some("m:limit"),
90 "tends-to" => Some("m:tendsto"),
91 "exponential" => Some("m:exp"),
93 "natural-logarithm" => Some("m:ln"),
94 "logarithm" => Some("m:log"),
95 "sine" => Some("m:sin"),
96 "cosine" => Some("m:cos"),
97 "tangent" => Some("m:tan"),
98 "secant" => Some("m:sec"),
99 "cosecant" => Some("m:csc"),
100 "cotangent" => Some("m:cot"),
101 "hyperbolic-sine" => Some("m:sinh"),
102 "hyperbolic-cosine" => Some("m:cosh"),
103 "hyperbolic-tangent" => Some("m:tanh"),
104 "hyperbolic-secant" => Some("m:sech"),
105 "hyperbolic-cosecant" => Some("m:csch"),
106 "hyperbolic-cotantent" => Some("m:coth"),
107 "inverse-sine" => Some("m:arcsin"),
108 "inverse-cosine" => Some("m:arccos"),
109 "inverse-tangent" => Some("m:arctan"),
110 "inverse-secant" => Some("m:arcsec"),
111 "inverse-cosecant" => Some("m:arccsc"),
112 "inverse-cotangent" => Some("m:arccot"),
113 "inverse-hyperbolic-sine" => Some("m:arcsinh"),
114 "inverse-hyperbolic-cosine" => Some("m:arccosh"),
115 "inverse-hyperbolic-tangent" => Some("m:arctanh"),
116 "inverse-hyperbolic-secant" => Some("m:arcsech"),
117 "inverse-hyperbolic-cosecant" => Some("m:arccsch"),
118 "inverse-hyperbolic-cotangent" => Some("m:arccoth"),
119 "mean" => Some("m:mean"),
121 "standard-deviation" => Some("m:sdev"),
122 "variance" => Some("m:var"),
123 "median" => Some("m:median"),
124 "mode" => Some("m:mode"),
125 "moment" => Some("m:moment"),
126 "determinant" => Some("m:determinant"),
128 "transpose" => Some("m:transpose"),
129 "selector" => Some("m:selector"),
130 "vector-product" => Some("m:vectorproduct"),
131 "scalar-product" => Some("m:scalarproduct"),
132 "outer-product" => Some("m:outerproduct"),
133 "integers" => Some("m:integers"),
135 "reals" => Some("m:reals"),
136 "rationals" => Some("m:rationals"),
137 "numbers" => Some("m:naturalnumbers"),
138 "complexes" => Some("m:complexes"),
139 "primes" => Some("m:primes"),
140 "exponential-e" => Some("m:exponentiale"),
141 "imaginary-i" => Some("m:imaginaryi"),
142 "notanumber" => Some("m:notanumber"),
143 "true" => Some("m:true"),
144 "false" => Some("m:false"),
145 "empty-set" => Some("m:emptyset"),
146 "circular-pi" => Some("m:pi"),
147 "Euler-constant" => Some("m:eulergamma"),
148 "infinity" => Some("m:infinity"),
149 "inverse" => Some("m:inverse"),
151 "lambda" => Some("m:lambda"),
152 "compose" => Some("m:compose"),
153 "identity" => Some("m:ident"),
154 "domain" => Some("m:domain"),
155 "codomain" => Some("m:codomain"),
156 "image" => Some("m:image"),
157 "square-root" => Some("m:root"),
158 _ => None,
159 }
160}
161
162pub fn convert_to_cmml(doc: &PostDocument, xmath: &Node) -> NodeData {
166 reset_share_counter();
167 super::presentation::bind_cmml_top_context(doc, xmath);
169 CMML_DEPTH.with(|d| d.set(0));
170 CMML_PATH.with(|p| p.borrow_mut().clear());
171 cmml_contents(doc, xmath)
172}
173
174const CMML_MAX_DEPTH: u32 = 256;
193thread_local! {
194 static CMML_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
195 static CMML_PATH: std::cell::RefCell<rustc_hash::FxHashSet<usize>>
196 = std::cell::RefCell::new(rustc_hash::FxHashSet::default());
197}
198
199enum CmmlEnter {
200 Ok,
201 DepthExceeded,
202 Cycle,
203}
204
205fn cmml_enter(node: &Node) -> CmmlEnter {
206 let ptr = node.node_ptr() as usize;
207 let in_path = CMML_PATH.with(|p| !p.borrow_mut().insert(ptr));
208 if in_path {
209 if std::env::var_os("LATEXML_CMML_TRACE_CYCLE").is_some() {
215 let id = node
216 .get_attribute("xml:id")
217 .or_else(|| node.get_attribute("id"))
218 .unwrap_or_else(|| format!("({})", node.get_name()));
219 eprintln!("cmml_cycle: re-entering {} ptr=0x{:x}", id, ptr);
220 }
221 return CmmlEnter::Cycle;
222 }
223 CMML_DEPTH.with(|d| {
224 let cur = d.get();
225 if cur >= CMML_MAX_DEPTH {
226 CMML_PATH.with(|p| {
228 p.borrow_mut().remove(&ptr);
229 });
230 CmmlEnter::DepthExceeded
231 } else {
232 d.set(cur + 1);
233 CmmlEnter::Ok
234 }
235 })
236}
237
238fn cmml_exit(node: &Node) {
239 CMML_PATH.with(|p| {
240 p.borrow_mut().remove(&(node.node_ptr() as usize));
241 });
242 CMML_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
243}
244
245fn cmml_contents(doc: &PostDocument, node: &Node) -> NodeData {
249 let children = element_children(node);
250 if children.is_empty() {
251 cmml_error("missing-subexpression")
252 } else if children.len() == 1 {
253 cmml(doc, &children[0])
254 } else {
255 cmml_unparsed(doc, &children)
256 }
257}
258
259fn cmml(doc: &PostDocument, node: &Node) -> NodeData {
263 match cmml_enter(node) {
264 CmmlEnter::Cycle => return cmml_error("cycle"),
265 CmmlEnter::DepthExceeded => return cmml_error("recursion-depth-exceeded"),
266 CmmlEnter::Ok => {},
267 }
268 let result = cmml_impl(doc, node);
269 cmml_exit(node);
270 result
271}
272
273fn cmml_impl(doc: &PostDocument, node: &Node) -> NodeData {
274 let tag = doc.get_qname(node).unwrap_or_default();
275
276 if tag == "ltx:XMRef" {
278 if let Some(idref) = node.get_attribute("idref") {
279 if let Some(target) = doc.find_node_by_id(&idref) {
280 return cmml(doc, target);
281 }
282 }
283 return cmml_error("unresolved-reference");
284 }
285
286 match tag.as_str() {
287 "ltx:XMDual" => {
288 let children = element_children(node);
289 if !children.is_empty() {
290 cmml(doc, &children[0]) } else {
292 cmml_error("empty-dual")
293 }
294 },
295 "ltx:XMWrap" | "ltx:XMArg" => cmml_contents(doc, node),
296 "ltx:XMApp" => {
297 if let Some(meaning) = node.get_attribute("meaning") {
299 return cmml_token_by_meaning(&meaning, node);
300 }
301 if node.get_attribute("role").as_deref() == Some("ID") {
303 return cmml_decorated_symbol(doc, node);
304 }
305 let children = element_children(node);
307 if children.is_empty() {
308 return cmml_error("missing-operator");
309 }
310 let op = &children[0];
311 let args = &children[1..];
312
313 let rop = if doc.is_qname(op, "ltx:XMRef") {
315 op.get_attribute("idref")
316 .and_then(|id| doc.find_node_by_id(&id).cloned())
317 .unwrap_or_else(|| op.clone())
318 } else {
319 op.clone()
320 };
321
322 let meaning = rop.get_attribute("meaning").unwrap_or_default();
323 let _role = rop.get_attribute("role").unwrap_or_default();
324
325 match meaning.as_str() {
327 "multirelation" if !args.is_empty() => {
331 let lhs0 = cmml(doc, &args[0]);
332 if args.len() == 1 {
333 return lhs0;
334 }
335 let mut lhs = lhs0;
336 let mut relations = Vec::new();
337 let mut i = 1;
338 while i < args.len() {
339 let rel = &args[i];
340 let Some(rhs) = args.get(i + 1) else { break };
341 relations.push(NodeData::Element {
342 tag: "m:apply".to_string(),
343 attributes: None,
344 children: vec![cmml(doc, rel), lhs, cmml_shared(doc, rhs)],
345 });
346 lhs = cmml_share(rhs);
347 i += 2;
348 }
349 if relations.len() > 1 {
350 let mut children = vec![NodeData::Element {
351 tag: "m:and".to_string(),
352 attributes: None,
353 children: vec![],
354 }];
355 children.extend(relations);
356 NodeData::Element {
357 tag: "m:apply".to_string(),
358 attributes: None,
359 children,
360 }
361 } else {
362 relations
363 .pop()
364 .unwrap_or_else(|| cmml_error("multirelation"))
365 }
366 },
367 "less-than-or-approximately-equals" if !args.is_empty() => {
371 let first: Vec<NodeData> = args.iter().map(|a| cmml_shared(doc, a)).collect();
372 let second: Vec<NodeData> = args.iter().map(cmml_share).collect();
373 let mk = |op: &str, ops: Vec<NodeData>| {
374 let mut children = vec![NodeData::Element {
375 tag: op.to_string(),
376 attributes: None,
377 children: vec![],
378 }];
379 children.extend(ops);
380 NodeData::Element {
381 tag: "m:apply".to_string(),
382 attributes: None,
383 children,
384 }
385 };
386 NodeData::Element {
387 tag: "m:or".to_string(),
388 attributes: None,
389 children: vec![mk("m:lt", first), mk("m:approx", second)],
390 }
391 },
392 "square-root" if !args.is_empty() => NodeData::Element {
393 tag: "m:apply".to_string(),
394 attributes: None,
395 children: vec![
396 NodeData::Element {
397 tag: "m:root".to_string(),
398 attributes: None,
399 children: vec![],
400 },
401 cmml(doc, &args[0]),
402 ],
403 },
404 "nth-root" if args.len() >= 2 => NodeData::Element {
405 tag: "m:apply".to_string(),
406 attributes: None,
407 children: vec![
408 NodeData::Element {
409 tag: "m:root".to_string(),
410 attributes: None,
411 children: vec![],
412 },
413 NodeData::Element {
414 tag: "m:degree".to_string(),
415 attributes: None,
416 children: vec![cmml(doc, &args[0])],
418 },
419 cmml(doc, &args[1]),
420 ],
421 },
422 "set" => {
423 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
424 NodeData::Element {
425 tag: "m:set".to_string(),
426 attributes: None,
427 children: items,
428 }
429 },
430 "list" => {
431 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
432 NodeData::Element {
433 tag: "m:list".to_string(),
434 attributes: None,
435 children: items,
436 }
437 },
438 "vector" => {
439 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
440 NodeData::Element {
441 tag: "m:vector".to_string(),
442 attributes: None,
443 children: items,
444 }
445 },
446 "open-interval" => {
448 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
449 NodeData::Element {
450 tag: "m:interval".to_string(),
451 attributes: Some(HashMap::from_iter([(
452 "closure".to_string(),
453 "open".to_string(),
454 )])),
455 children: items,
456 }
457 },
458 "closed-interval" => {
459 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
460 NodeData::Element {
461 tag: "m:interval".to_string(),
462 attributes: Some(HashMap::from_iter([(
463 "closure".to_string(),
464 "closed".to_string(),
465 )])),
466 children: items,
467 }
468 },
469 "closed-open-interval" => {
470 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
471 NodeData::Element {
472 tag: "m:interval".to_string(),
473 attributes: Some(HashMap::from_iter([(
474 "closure".to_string(),
475 "closed-open".to_string(),
476 )])),
477 children: items,
478 }
479 },
480 "open-closed-interval" => {
481 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
482 NodeData::Element {
483 tag: "m:interval".to_string(),
484 attributes: Some(HashMap::from_iter([(
485 "closure".to_string(),
486 "open-closed".to_string(),
487 )])),
488 children: items,
489 }
490 },
491 "contains" | "superset-of" | "superset-of-or-equals" | "superset-of-and-not-equals" => {
493 let cmml_op = match meaning.as_str() {
494 "contains" => "m:in",
495 "superset-of" | "superset-of-or-equals" => "m:subset",
496 "superset-of-and-not-equals" => "m:prsubset",
497 _ => "m:subset",
498 };
499 let mut reversed_args: Vec<NodeData> = args.iter().rev().map(|a| cmml(doc, a)).collect();
500 reversed_args.insert(0, NodeData::Element {
501 tag: cmml_op.to_string(),
502 attributes: None,
503 children: vec![],
504 });
505 NodeData::Element {
506 tag: "m:apply".to_string(),
507 attributes: None,
508 children: reversed_args,
509 }
510 },
511 "not-contains" => {
513 let mut reversed_args: Vec<NodeData> = args.iter().rev().map(|a| cmml(doc, a)).collect();
514 reversed_args.insert(0, NodeData::Element {
515 tag: "m:notin".to_string(),
516 attributes: None,
517 children: vec![],
518 });
519 NodeData::Element {
520 tag: "m:apply".to_string(),
521 attributes: None,
522 children: reversed_args,
523 }
524 },
525 "not-approximately-equals" => {
527 let inner_args: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
528 let mut apply_children = vec![NodeData::Element {
529 tag: "m:approx".to_string(),
530 attributes: None,
531 children: vec![],
532 }];
533 apply_children.extend(inner_args);
534 NodeData::Element {
535 tag: "m:apply".to_string(),
536 attributes: None,
537 children: vec![
538 NodeData::Element {
539 tag: "m:not".to_string(),
540 attributes: None,
541 children: vec![],
542 },
543 NodeData::Element {
544 tag: "m:apply".to_string(),
545 attributes: None,
546 children: apply_children,
547 },
548 ],
549 }
550 },
551 "hack-definite-integral" if args.len() >= 4 => NodeData::Element {
553 tag: "m:apply".to_string(),
554 attributes: None,
555 children: vec![
556 NodeData::Element {
557 tag: "m:int".to_string(),
558 attributes: None,
559 children: vec![],
560 },
561 NodeData::Element {
562 tag: "m:bvar".to_string(),
563 attributes: None,
564 children: vec![cmml(doc, &args[3])],
565 },
566 NodeData::Element {
567 tag: "m:lowlimit".to_string(),
568 attributes: None,
569 children: vec![cmml(doc, &args[0])],
570 },
571 NodeData::Element {
572 tag: "m:uplimit".to_string(),
573 attributes: None,
574 children: vec![cmml(doc, &args[1])],
575 },
576 cmml(doc, &args[2]),
577 ],
578 },
579 "formulae" => {
581 let items: Vec<NodeData> = args.iter().map(|a| cmml(doc, a)).collect();
582 let mut children = vec![NodeData::Element {
583 tag: "m:csymbol".to_string(),
584 attributes: Some(HashMap::from_iter([(
585 "cd".to_string(),
586 "ambiguous".to_string(),
587 )])),
588 children: vec![NodeData::Text("formulae-sequence".to_string())],
589 }];
590 children.extend(items);
591 NodeData::Element {
592 tag: "m:apply".to_string(),
593 attributes: None,
594 children,
595 }
596 },
597 _ => {
598 let mut apply_children = vec![cmml(doc, op)];
600 apply_children.extend(args.iter().map(|a| cmml(doc, a)));
601 NodeData::Element {
602 tag: "m:apply".to_string(),
603 attributes: None,
604 children: apply_children,
605 }
606 },
607 }
608 },
609 "ltx:XMTok" => cmml_leaf(doc, node),
610 "ltx:XMHint" => {
611 NodeData::Text(String::new())
613 },
614 "ltx:XMArray" => cmml_array(doc, node),
615 "ltx:XMText" => cmml_decorated_symbol(doc, node),
616 _ => cmml_decorated_symbol(doc, node),
617 }
618}
619
620fn cmml_leaf(_doc: &PostDocument, node: &Node) -> NodeData {
624 let meaning = node.get_attribute("meaning");
625 let role = node.get_attribute("role").unwrap_or_default();
626
627 if let Some(ref m) = meaning {
628 if let Some(element_name) = meaning_to_cmml_element(m) {
630 return NodeData::Element {
631 tag: element_name.to_string(),
632 attributes: None,
633 children: vec![],
634 };
635 }
636
637 if let Some(cd) = node.get_attribute("omcd") {
639 return NodeData::Element {
640 tag: "m:csymbol".to_string(),
641 attributes: Some(HashMap::from_iter([("cd".to_string(), cd)])),
642 children: vec![NodeData::Text(m.clone())],
643 };
644 }
645
646 if role == "NUMBER" {
648 let cn_type = if is_perl_integer(m) {
650 "integer"
651 } else {
652 "float"
653 };
654 return NodeData::Element {
655 tag: "m:cn".to_string(),
656 attributes: Some(HashMap::from_iter([(
657 "type".to_string(),
658 cn_type.to_string(),
659 )])),
660 children: vec![NodeData::Text(m.clone())],
661 };
662 }
663
664 return NodeData::Element {
666 tag: "m:csymbol".to_string(),
667 attributes: Some(HashMap::from_iter([(
668 "cd".to_string(),
669 "latexml".to_string(),
670 )])),
671 children: vec![NodeData::Text(m.clone())],
672 };
673 }
674
675 let content = node.get_content();
677 match role.as_str() {
678 "NUMBER" => {
679 let cn_type = if is_perl_integer(&content) {
680 "integer"
681 } else {
682 "float"
683 };
684 NodeData::Element {
685 tag: "m:cn".to_string(),
686 attributes: Some(HashMap::from_iter([(
687 "type".to_string(),
688 cn_type.to_string(),
689 )])),
690 children: vec![NodeData::Text(content)],
691 }
692 },
693 "SUPERSCRIPTOP" => NodeData::Element {
694 tag: "m:csymbol".to_string(),
695 attributes: Some(HashMap::from_iter([(
696 "cd".to_string(),
697 "ambiguous".to_string(),
698 )])),
699 children: vec![NodeData::Text("superscript".to_string())],
700 },
701 "SUBSCRIPTOP" => NodeData::Element {
702 tag: "m:csymbol".to_string(),
703 attributes: Some(HashMap::from_iter([(
704 "cd".to_string(),
705 "ambiguous".to_string(),
706 )])),
707 children: vec![NodeData::Text("subscript".to_string())],
708 },
709 _ => {
710 let name = if content.is_empty() {
712 node
713 .get_attribute("name")
714 .unwrap_or_else(|| "?".to_string())
715 } else {
716 content
717 };
718 NodeData::Element {
719 tag: "m:ci".to_string(),
720 attributes: None,
721 children: vec![NodeData::Text(stylize_ci_content(node, name))],
722 }
723 },
724 }
725}
726
727fn cmml_token_by_meaning(meaning: &str, _node: &Node) -> NodeData {
729 if let Some(element_name) = meaning_to_cmml_element(meaning) {
730 NodeData::Element {
731 tag: element_name.to_string(),
732 attributes: None,
733 children: vec![],
734 }
735 } else {
736 NodeData::Element {
737 tag: "m:csymbol".to_string(),
738 attributes: Some(HashMap::from_iter([(
739 "cd".to_string(),
740 "latexml".to_string(),
741 )])),
742 children: vec![NodeData::Text(meaning.to_string())],
743 }
744 }
745}
746
747thread_local! {
751 static SH_COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
752 static SH_GLOBAL_COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
755}
756pub(super) fn reset_share_counter() { SH_COUNTER.with(|c| c.set(0)); }
757
758fn cmml_shared(doc: &PostDocument, node: &Node) -> NodeData {
762 if crate::document::get_xml_id(node).is_none() {
763 let mut parent = node.get_parent();
766 while let Some(ref p) = parent {
767 if crate::document::get_xml_id(p).is_some() {
768 break;
769 }
770 parent = p.get_parent();
771 }
772 let pid = parent
773 .as_ref()
774 .and_then(crate::document::get_xml_id)
775 .map(|id| format!("{id}."))
776 .unwrap_or_default();
777 let n = if pid.is_empty() {
784 SH_GLOBAL_COUNTER.with(|c| {
785 let v = c.get() + 1;
786 c.set(v);
787 v
788 })
789 } else {
790 SH_COUNTER.with(|c| {
791 let v = c.get() + 1;
792 c.set(v);
793 v
794 })
795 };
796 let mut handle = node.clone();
797 handle.set_attribute("xml:id", &format!("{pid}sh{n}")).ok();
798 if let Some(pfragid) = parent.as_ref().and_then(|p| p.get_attribute("fragid")) {
799 handle
800 .set_attribute("fragid", &format!("{pfragid}.sh{n}"))
801 .ok();
802 }
803 }
804 cmml(doc, node)
805}
806
807fn cmml_share(node: &Node) -> NodeData {
809 if let Some(fragid) = node.get_attribute("fragid") {
810 NodeData::Element {
813 tag: "m:share".to_string(),
814 attributes: Some(HashMap::from_iter([(
815 "href".to_string(),
816 format!("#{fragid}"),
817 )])),
818 children: vec![],
819 }
820 } else {
821 crate::Warn!(
822 "expected",
823 "fragid",
824 "Shared node is missing fragid (multirelation/or-compose share)"
825 );
826 NodeData::Element {
827 tag: "m:share".to_string(),
828 attributes: None,
829 children: vec![],
830 }
831 }
832}
833
834fn is_perl_integer(s: &str) -> bool {
836 let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
837 !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
838}
839
840fn stylize_ci_content(node: &Node, text: String) -> String {
845 if text.chars().all(|c| {
848 matches!(c,
849 '\u{00AD}' | '\u{200B}'..='\u{200F}' | '\u{2060}'..='\u{2064}' | '\u{FEFF}')
850 }) {
851 return text;
852 }
853 let Some(font) = node
855 .get_attribute("font")
856 .or_else(super::presentation::ctx_font)
857 else {
858 return text;
859 };
860 let variant = crate::unicode::unicode_mathvariant(&font);
861 if variant.is_empty() || variant == "normal" {
862 return text;
863 }
864 if let Some(u) = crate::unicode::unicode_convert(&text, variant)
865 && !u.is_empty()
866 {
867 return u;
868 }
869 format!("{variant}-{text}")
870}
871
872fn cmml_decorated_symbol(doc: &PostDocument, node: &Node) -> NodeData {
878 if let Some(meaning) = node.get_attribute("meaning") {
879 let cd = node
880 .get_attribute("omcd")
881 .unwrap_or_else(|| "latexml".to_string());
882 return NodeData::Element {
883 tag: "m:csymbol".to_string(),
884 attributes: Some(HashMap::from_iter([("cd".to_string(), cd)])),
885 children: vec![NodeData::Text(meaning)],
886 };
887 }
888 NodeData::Element {
889 tag: "m:ci".to_string(),
890 attributes: None,
891 children: vec![super::presentation::pmml_for_ci(doc, node)],
892 }
893}
894
895fn cmml_array(doc: &PostDocument, node: &Node) -> NodeData {
899 let meaning = node.get_attribute("meaning").unwrap_or_default();
900
901 if meaning == "cases" {
902 let mut pieces = Vec::new();
904 let mut otherwises = Vec::new();
905
906 for row in element_children(node) {
907 let items = element_children(&row);
908 if items.is_empty() {
909 continue;
910 }
911 if items.len() == 1 {
912 otherwises.push(cmml_contents(doc, &items[0]));
913 } else if items[1].get_content().contains("otherwise") {
914 otherwises.push(cmml_contents(doc, &items[0]));
915 } else {
916 pieces.push(NodeData::Element {
917 tag: "m:piece".to_string(),
918 attributes: None,
919 children: vec![cmml_contents(doc, &items[0]), cmml_contents(doc, &items[1])],
920 });
921 }
922 }
923
924 if let Some(ow) = otherwises.into_iter().next() {
925 pieces.push(NodeData::Element {
926 tag: "m:otherwise".to_string(),
927 attributes: None,
928 children: vec![ow],
929 });
930 }
931
932 NodeData::Element {
933 tag: "m:piecewise".to_string(),
934 attributes: None,
935 children: pieces,
936 }
937 } else {
938 let mut rows = Vec::new();
940 for row in element_children(node) {
941 let cells: Vec<NodeData> = element_children(&row)
942 .iter()
943 .map(|cell| cmml_contents(doc, cell))
944 .collect();
945 rows.push(NodeData::Element {
946 tag: "m:matrixrow".to_string(),
947 attributes: None,
948 children: cells,
949 });
950 }
951 NodeData::Element {
952 tag: "m:matrix".to_string(),
953 attributes: None,
954 children: rows,
955 }
956 }
957}
958
959fn cmml_unparsed(doc: &PostDocument, nodes: &[Node]) -> NodeData {
963 let mut results = vec![NodeData::Element {
964 tag: "m:csymbol".to_string(),
965 attributes: Some(HashMap::from_iter([(
966 "cd".to_string(),
967 "ambiguous".to_string(),
968 )])),
969 children: vec![NodeData::Text("fragments".to_string())],
970 }];
971
972 for node in nodes {
973 let tag = doc.get_qname(node).unwrap_or_default();
974 if tag == "ltx:XMTok" && node.get_attribute("role").as_deref() == Some("UNKNOWN") {
975 results.push(NodeData::Element {
976 tag: "m:csymbol".to_string(),
977 attributes: Some(HashMap::from_iter([(
978 "cd".to_string(),
979 "unknown".to_string(),
980 )])),
981 children: vec![NodeData::Text(node.get_content())],
982 });
983 } else {
984 results.push(cmml(doc, node));
985 }
986 }
987
988 NodeData::Element {
989 tag: "m:cerror".to_string(),
990 attributes: None,
991 children: results,
992 }
993}
994
995fn cmml_error(symbol: &str) -> NodeData {
997 NodeData::Element {
998 tag: "m:cerror".to_string(),
999 attributes: None,
1000 children: vec![NodeData::Element {
1001 tag: "m:csymbol".to_string(),
1002 attributes: Some(HashMap::from_iter([(
1003 "cd".to_string(),
1004 "ambiguous".to_string(),
1005 )])),
1006 children: vec![NodeData::Text(symbol.to_string())],
1007 }],
1008 }
1009}