Skip to main content

latexml_post/
open_math.rs

1//! OpenMath conversion processor.
2//!
3//! Port of `LaTeXML::Post::OpenMath`.
4//! Converts XMath nodes into OpenMath XML representation.
5//! Uses a converter table (DefOpenMath) for dispatching Token/Apply conversion.
6
7use libxml::tree::Node;
8use rustc_hash::FxHashMap as HashMap;
9
10use crate::{
11  document::{NodeData, PostDocument, element_children},
12  math_processor::{MathConversion, MathProcessor, math_is_parsed},
13  processor::{ProcessResult, Processor},
14};
15
16const OM_URI: &str = "http://www.openmath.org/OpenMath";
17const OM_MIMETYPE: &str = "application/openmath+xml";
18
19/// OpenMath converter table entry.
20type OmConverter = fn(&PostDocument, &Node) -> NodeData;
21
22/// OpenMath post-processor.
23///
24/// Port of `LaTeXML::Post::OpenMath`.
25pub struct OpenMath {
26  name:         String,
27  is_secondary: bool,
28  hack_plane1:  bool,
29  plane1:       bool,
30}
31
32impl Default for OpenMath {
33  fn default() -> Self { Self::new() }
34}
35
36impl OpenMath {
37  pub fn new() -> Self {
38    OpenMath {
39      name:         "OpenMath".to_string(),
40      is_secondary: false,
41      hack_plane1:  false,
42      plane1:       true,
43    }
44  }
45}
46
47impl Processor for OpenMath {
48  fn get_name(&self) -> &str { &self.name }
49
50  fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
51    doc.findnodes("//ltx:Math[not(ancestor::ltx:Math)]")
52  }
53
54  fn process(&mut self, doc: PostDocument, _nodes: Vec<Node>) -> ProcessResult { Ok(vec![doc]) }
55}
56
57impl MathProcessor for OpenMath {
58  fn convert_node(&self, doc: &PostDocument, xmath: &Node) -> Option<MathConversion> {
59    let children = element_children(xmath);
60    let xml = if children.len() == 1 {
61      om_expr(doc, &children[0])
62    } else {
63      om_unparsed(doc, &children)
64    };
65
66    Some(MathConversion {
67      processor_name: self.name.clone(),
68      mimetype:       Some(OM_MIMETYPE.to_string()),
69      xml:            Some(xml),
70      string:         None,
71      src:            None,
72      width:          None,
73      height:         None,
74      depth:          None,
75    })
76  }
77
78  fn combine_parallel(
79    &self,
80    _doc: &PostDocument,
81    _xmath: &Node,
82    primary: MathConversion,
83    secondaries: Vec<MathConversion>,
84  ) -> MathConversion {
85    let mut attr_children = Vec::new();
86
87    for secondary in &secondaries {
88      let mimetype = secondary.mimetype.as_deref().unwrap_or("unknown");
89      attr_children.push(NodeData::Element {
90        tag:        "om:OMS".to_string(),
91        attributes: Some(HashMap::from_iter([
92          ("cd".to_string(), "Alternate".to_string()),
93          ("name".to_string(), mimetype.to_string()),
94        ])),
95        children:   vec![],
96      });
97
98      if mimetype == OM_MIMETYPE {
99        if let Some(ref xml) = secondary.xml {
100          attr_children.push(xml.clone());
101        }
102      } else if let Some(ref xml) = secondary.xml {
103        attr_children.push(NodeData::Element {
104          tag:        "om:OMFOREIGN".to_string(),
105          attributes: None,
106          children:   vec![xml.clone()],
107        });
108      } else if let Some(ref string) = secondary.string {
109        attr_children.push(NodeData::Element {
110          tag:        "om:OMSTR".to_string(),
111          attributes: None,
112          children:   vec![NodeData::Text(string.clone())],
113        });
114      }
115    }
116
117    if let Some(ref xml) = primary.xml {
118      attr_children.push(xml.clone());
119    }
120
121    MathConversion {
122      processor_name: self.name.clone(),
123      mimetype:       Some(OM_MIMETYPE.to_string()),
124      xml:            Some(NodeData::Element {
125        tag:        "om:OMATTR".to_string(),
126        attributes: None,
127        children:   attr_children,
128      }),
129      string:         None,
130      src:            None,
131      width:          None,
132      height:         None,
133      depth:          None,
134    }
135  }
136
137  fn outer_wrapper(&self, _doc: &PostDocument, _xmath: &Node, conversion: NodeData) -> NodeData {
138    NodeData::Element {
139      tag:        "om:OMOBJ".to_string(),
140      attributes: None,
141      children:   vec![conversion],
142    }
143  }
144
145  fn raw_id_suffix(&self) -> &str { ".om" }
146
147  fn is_secondary(&self) -> bool { self.is_secondary }
148
149  fn can_convert(&self, _doc: &PostDocument, math: &Node) -> bool { math_is_parsed(math) }
150
151  fn preprocess(&self, _doc: &PostDocument, _nodes: &[Node]) {
152    // Register om namespace (would need &mut doc)
153    log::trace!("OpenMath: would register om namespace");
154  }
155}
156
157// ======================================================================
158// OpenMath expression conversion
159
160/// Convert an XMath element node to OpenMath.
161///
162/// Port of `om_expr`.
163fn om_expr(doc: &PostDocument, node: &Node) -> NodeData {
164  // Realize XMRef nodes
165  let real_node = if doc.is_qname(node, "ltx:XMRef") {
166    if let Some(idref) = node.get_attribute("idref") {
167      doc.find_node_by_id(&idref).cloned()
168    } else {
169      None
170    }
171  } else {
172    Some(node.clone())
173  };
174
175  match real_node {
176    Some(ref n) => om_expr_aux(doc, n),
177    None => om_error("Missing Subexpression"),
178  }
179}
180
181/// Core OpenMath expression conversion.
182///
183/// Port of `om_expr_aux`.
184fn om_expr_aux(doc: &PostDocument, node: &Node) -> NodeData {
185  let tag = match doc.get_qname(node) {
186    Some(t) => t,
187    None => return om_error("Missing Subexpression"),
188  };
189
190  match tag.as_str() {
191    "ltx:XMWrap" | "ltx:XMArg" => {
192      let children = element_children(node);
193      if children.len() == 1 {
194        om_expr(doc, &children[0])
195      } else {
196        om_unparsed(doc, &children)
197      }
198    },
199    "ltx:XMDual" => {
200      let children = element_children(node);
201      if !children.is_empty() {
202        om_expr(doc, &children[0]) // Content branch
203      } else {
204        om_error("Empty XMDual")
205      }
206    },
207    "ltx:XMApp" => {
208      let children = element_children(node);
209      if children.is_empty() {
210        return om_error("Missing Operator");
211      }
212      // Generic application
213      let mut oma_children = Vec::new();
214      for child in &children {
215        oma_children.push(om_expr(doc, child));
216      }
217      NodeData::Element {
218        tag:        "om:OMA".to_string(),
219        attributes: None,
220        children:   oma_children,
221      }
222    },
223    "ltx:XMTok" => {
224      if let Some(meaning) = node.get_attribute("meaning") {
225        let cd = node
226          .get_attribute("omcd")
227          .unwrap_or_else(|| "latexml".to_string());
228        NodeData::Element {
229          tag:        "om:OMS".to_string(),
230          attributes: Some(HashMap::from_iter([
231            ("name".to_string(), meaning),
232            ("cd".to_string(), cd),
233          ])),
234          children:   vec![],
235        }
236      } else {
237        // Variable
238        let name = node.get_content();
239        let name = if name.trim().is_empty() {
240          node
241            .get_attribute("name")
242            .unwrap_or_else(|| "?".to_string())
243        } else {
244          name
245        };
246        NodeData::Element {
247          tag:        "om:OMV".to_string(),
248          attributes: Some(HashMap::from_iter([("name".to_string(), name)])),
249          children:   vec![],
250        }
251      }
252    },
253    "ltx:XMHint" => {
254      // Hints are ignored in OpenMath
255      NodeData::Text(String::new())
256    },
257    "ltx:XMText" => {
258      let text = node.get_content();
259      NodeData::Element {
260        tag:        "om:OMSTR".to_string(),
261        attributes: None,
262        children:   vec![NodeData::Text(text)],
263      }
264    },
265    _ => {
266      let text = node.get_content();
267      NodeData::Element {
268        tag:        "om:OMSTR".to_string(),
269        attributes: None,
270        children:   vec![NodeData::Text(text)],
271      }
272    },
273  }
274}
275
276/// Convert unparsed (multiple) nodes to OpenMath error expression.
277fn om_unparsed(doc: &PostDocument, nodes: &[Node]) -> NodeData {
278  if nodes.is_empty() {
279    return om_error("Missing Subexpression");
280  }
281
282  let mut children = vec![NodeData::Element {
283    tag:        "om:OMS".to_string(),
284    attributes: Some(HashMap::from_iter([
285      ("cd".to_string(), "ambiguous".to_string()),
286      ("name".to_string(), "fragments".to_string()),
287    ])),
288    children:   vec![],
289  }];
290
291  for node in nodes {
292    let tag = doc.get_qname(node).unwrap_or_default();
293    if tag == "ltx:XMHint" {
294      continue;
295    }
296    children.push(om_expr_aux(doc, node));
297  }
298
299  NodeData::Element {
300    tag: "om:OME".to_string(),
301    attributes: None,
302    children,
303  }
304}
305
306/// Create an OpenMath error element.
307fn om_error(msg: &str) -> NodeData {
308  NodeData::Element {
309    tag:        "om:OME".to_string(),
310    attributes: None,
311    children:   vec![
312      NodeData::Element {
313        tag:        "om:OMS".to_string(),
314        attributes: Some(HashMap::from_iter([
315          ("name".to_string(), "unexpected".to_string()),
316          ("cd".to_string(), "moreerrors".to_string()),
317        ])),
318        children:   vec![],
319      },
320      NodeData::Element {
321        tag:        "om:OMSTR".to_string(),
322        attributes: None,
323        children:   vec![NodeData::Text(msg.to_string())],
324      },
325    ],
326  }
327}
328
329#[cfg(test)]
330mod tests {
331  use super::*;
332
333  #[test]
334  fn openmath_new_has_default_name() {
335    let o = OpenMath::new();
336    assert_eq!(o.get_name(), "OpenMath");
337    assert!(!o.is_secondary);
338    assert!(!o.hack_plane1);
339    assert!(o.plane1, "plane1 defaults to true");
340  }
341
342  #[test]
343  fn openmath_default_matches_new() {
344    let a = OpenMath::default();
345    let b = OpenMath::new();
346    assert_eq!(a.get_name(), b.get_name());
347    assert_eq!(a.is_secondary, b.is_secondary);
348    assert_eq!(a.plane1, b.plane1);
349    assert_eq!(a.hack_plane1, b.hack_plane1);
350  }
351
352  #[test]
353  fn openmath_raw_id_suffix() {
354    let o = OpenMath::new();
355    assert_eq!(o.raw_id_suffix(), ".om");
356  }
357
358  #[test]
359  fn openmath_is_secondary_false_by_default() {
360    let o = OpenMath::new();
361    assert!(!o.is_secondary());
362  }
363
364  #[test]
365  fn om_constants() {
366    assert_eq!(OM_URI, "http://www.openmath.org/OpenMath");
367    assert_eq!(OM_MIMETYPE, "application/openmath+xml");
368  }
369}