1use 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
19type OmConverter = fn(&PostDocument, &Node) -> NodeData;
21
22pub 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 log::trace!("OpenMath: would register om namespace");
154 }
155}
156
157fn om_expr(doc: &PostDocument, node: &Node) -> NodeData {
164 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
181fn 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]) } 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 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 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 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
276fn 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
306fn 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}