Skip to main content

latexml_post/
xmath.rs

1//! XMath pseudo-generator for preserving LaTeXML's parsed math.
2//!
3//! Port of `LaTeXML::Post::XMath`.
4//!
5//! If XMath is the primary representation (or only one), it is left in place.
6//! If secondary, it is cloned with modified IDs and moved.
7//! Must be the last math formatter in the chain when used as secondary,
8//! since XMath removal would break subsequent formatters.
9
10use libxml::tree::Node;
11use rustc_hash::FxHashMap as HashMap;
12
13use crate::{
14  document::{NodeData, PostDocument, element_children_iter},
15  math_processor::{MathConversion, MathProcessor},
16  processor::{ProcessResult, Processor},
17};
18
19const XMATH_MIMETYPE: &str = "application/x-latexml";
20
21/// XMath post-processor: preserves or clones XMath as a math representation.
22///
23/// Port of `LaTeXML::Post::XMath`.
24pub struct XMath {
25  name:         String,
26  is_secondary: bool,
27}
28
29impl Default for XMath {
30  fn default() -> Self { Self::new() }
31}
32
33impl XMath {
34  pub fn new() -> Self {
35    XMath {
36      name:         "XMath".to_string(),
37      is_secondary: false,
38    }
39  }
40}
41
42impl Processor for XMath {
43  fn get_name(&self) -> &str { &self.name }
44
45  fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
46    doc.findnodes("//ltx:Math[not(ancestor::ltx:Math)]")
47  }
48
49  fn process(&mut self, doc: PostDocument, _nodes: Vec<Node>) -> ProcessResult { Ok(vec![doc]) }
50}
51
52impl MathProcessor for XMath {
53  fn convert_node(&self, _doc: &PostDocument, xmath: &Node) -> Option<MathConversion> {
54    let id_suffix = self.id_suffix();
55    let xml = if !id_suffix.is_empty() {
56      // Secondary: clone the XMath with modified IDs
57      let children: Vec<NodeData> = element_children_iter(xmath)
58        .map(NodeData::XmlNode)
59        .collect();
60      Some(NodeData::Element {
61        tag: "ltx:XMath".to_string(),
62        attributes: Some(HashMap::from_iter([(
63          "_sourced".to_string(),
64          "1".to_string(),
65        )])),
66        children,
67      })
68    } else {
69      // Primary: just reference the existing XMath node
70      Some(NodeData::XmlNode(xmath.clone()))
71    };
72
73    Some(MathConversion {
74      processor_name: self.name.clone(),
75      mimetype: Some(XMATH_MIMETYPE.to_string()),
76      xml,
77      string: None,
78      src: None,
79      width: None,
80      height: None,
81      depth: None,
82    })
83  }
84
85  fn combine_parallel(
86    &self,
87    _doc: &PostDocument,
88    _xmath: &Node,
89    primary: MathConversion,
90    secondaries: Vec<MathConversion>,
91  ) -> MathConversion {
92    let mut alt_children = Vec::new();
93
94    // Primary XML goes first
95    if let Some(ref xml) = primary.xml {
96      alt_children.push(xml.clone());
97    }
98
99    // Add secondaries
100    for secondary in &secondaries {
101      let mimetype = secondary.mimetype.as_deref().unwrap_or("unknown");
102      if mimetype == XMATH_MIMETYPE {
103        if let Some(ref xml) = secondary.xml {
104          alt_children.push(xml.clone());
105        }
106      } else if let Some(ref xml) = secondary.xml {
107        // Other XML: needs wrapping (outerWrapper would be called by the processor)
108        alt_children.push(xml.clone());
109      }
110    }
111
112    MathConversion {
113      processor_name: self.name.clone(),
114      mimetype:       Some(XMATH_MIMETYPE.to_string()),
115      xml:            Some(NodeData::Element {
116        tag:        "_Fragment_".to_string(),
117        attributes: None,
118        children:   alt_children,
119      }),
120      string:         None,
121      src:            None,
122      width:          None,
123      height:         None,
124      depth:          None,
125    }
126  }
127
128  fn raw_id_suffix(&self) -> &str { ".xm" }
129
130  fn is_secondary(&self) -> bool { self.is_secondary }
131}
132
133#[cfg(test)]
134mod tests {
135  use super::*;
136
137  #[test]
138  fn xmath_new_has_default_name() {
139    let x = XMath::new();
140    assert_eq!(x.get_name(), "XMath");
141    assert!(!x.is_secondary);
142  }
143
144  #[test]
145  fn xmath_default_matches_new() {
146    let a = XMath::default();
147    let b = XMath::new();
148    assert_eq!(a.get_name(), b.get_name());
149    assert_eq!(a.is_secondary, b.is_secondary);
150  }
151
152  #[test]
153  fn xmath_raw_id_suffix() {
154    let x = XMath::new();
155    assert_eq!(x.raw_id_suffix(), ".xm");
156  }
157
158  #[test]
159  fn xmath_is_secondary_false_by_default() {
160    let x = XMath::new();
161    assert!(!x.is_secondary());
162  }
163
164  #[test]
165  fn xmath_mimetype_is_x_latexml() {
166    assert_eq!(XMATH_MIMETYPE, "application/x-latexml");
167  }
168}