1use libxml::tree::Node;
13
14use crate::{
15 document::{PostDocument, element_children},
16 math_processor::{MathConversion, MathProcessor},
17 processor::{ProcessResult, Processor},
18};
19
20const UNICODE_MATH_MIMETYPE: &str = "application/x-unicodemath";
21
22const PREC_RELOP: i32 = 1;
24const PREC_ADDOP: i32 = 2;
25const PREC_MULOP: i32 = 3;
26const PREC_SCRIPTOP: i32 = 4;
27const PREC_SYMBOL: i32 = 10;
28
29pub struct UnicodeMath {
33 name: String,
34 is_secondary: bool,
35}
36
37impl Default for UnicodeMath {
38 fn default() -> Self { Self::new() }
39}
40
41impl UnicodeMath {
42 pub fn new() -> Self {
43 UnicodeMath {
44 name: "UnicodeMath".to_string(),
45 is_secondary: false,
46 }
47 }
48}
49
50impl Processor for UnicodeMath {
51 fn get_name(&self) -> &str { &self.name }
52
53 fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
54 doc.findnodes("//ltx:Math[not(ancestor::ltx:Math)]")
55 }
56
57 fn process(&mut self, doc: PostDocument, _nodes: Vec<Node>) -> ProcessResult { Ok(vec![doc]) }
58}
59
60impl MathProcessor for UnicodeMath {
61 fn convert_node(&self, doc: &PostDocument, xmath: &Node) -> Option<MathConversion> {
62 let math = xmath.get_parent()?;
63 let (string, _prec) = unimath_internal(doc, &math);
64 Some(MathConversion {
65 processor_name: self.name.clone(),
66 mimetype: Some(UNICODE_MATH_MIMETYPE.to_string()),
67 xml: None,
68 string: Some(string),
69 src: None,
70 width: None,
71 height: None,
72 depth: None,
73 })
74 }
75
76 fn raw_id_suffix(&self) -> &str { ".muni" }
77
78 fn is_secondary(&self) -> bool { self.is_secondary }
79}
80
81pub fn unicodemath(doc: &PostDocument, node: &Node) -> String {
85 let (uni, _prec) = unimath_internal(doc, node);
86 uni
87}
88
89fn infix_prec(role: &str) -> Option<i32> {
93 match role {
94 "ADDOP" | "BINOP" => Some(PREC_ADDOP),
95 "MULOP" | "MIDDLE" | "COMPOSEOP" | "MODIFIEROP" => Some(PREC_MULOP),
96 "RELOP" | "METARELOP" | "ARROW" => Some(PREC_RELOP),
97 _ => None,
98 }
99}
100
101fn get_operator_role(doc: &PostDocument, node: &Node) -> Option<String> {
105 if let Some(role) = node.get_attribute("role") {
106 return Some(role);
107 }
108 if doc.is_qname(node, "ltx:XMApp") {
109 let children = element_children(node);
110 if children.len() >= 2 {
111 let op_role = children[0].get_attribute("role").unwrap_or_default();
112 if matches!(
113 op_role.as_str(),
114 "SUPERSCRIPTOP" | "SUBSCRIPTOP" | "OVERACCENT" | "UNDERACCENT" | "MODIFIER" | "MODIFIEROP"
115 ) {
116 return get_operator_role(doc, &children[1]);
117 }
118 }
119 }
120 None
121}
122
123fn realize(doc: &PostDocument, node: &Node) -> Option<Node> { doc.realize_xm_node(node) }
125
126fn unimath_internal(doc: &PostDocument, node: &Node) -> (String, i32) {
133 let tag = doc.get_qname(node).unwrap_or_default();
134 let role = node.get_attribute("role").unwrap_or_default();
135
136 match tag.as_str() {
137 "ltx:Math" | "ltx:XMath" => unimath_map(doc, &element_children(node)),
138 "ltx:XMDual" => {
139 let children = element_children(node);
140 if children.len() >= 2 {
141 unimath_internal(doc, &children[1]) } else {
143 unimath_error("Empty XMDual")
144 }
145 },
146 "ltx:XMWrap" | "ltx:XMArg" => unimath_map(doc, &element_children(node)),
147 "ltx:XMApp" => {
148 let children = element_children(node);
149 if children.is_empty() {
150 return unimath_error("Missing Operator");
151 }
152 let op = &children[0];
153 let args = &children[1..];
154
155 if role.contains("SUBSCRIPT") {
157 return unimath_sub(doc, None, op);
158 }
159 if role.contains("SUPERSCRIPT") {
160 return unimath_sup(doc, None, op);
161 }
162
163 let rop = realize(doc, op).unwrap_or_else(|| op.clone());
165 let op_role = get_operator_role(doc, &rop).unwrap_or_default();
166 let meaning = rop.get_attribute("meaning").unwrap_or_default();
167
168 match (op_role.as_str(), meaning.as_str()) {
170 (_, "formulae") | (_, "multirelation") => unimath_map(doc, args),
171 (_, "limit-from") | (_, "annotated") => unimath_prefix(doc, op, args),
172 (_, "square-root") => {
173 if !args.is_empty() {
174 let inner = unimath_nested(doc, &args[0], PREC_MULOP);
175 (format!("\u{221A}{}", inner), PREC_MULOP)
176 } else {
177 ("\u{221A}".to_string(), PREC_MULOP)
178 }
179 },
180 (_, "nth-root") => {
181 if args.len() >= 2 {
182 let (n, _) = unimath_internal(doc, &args[0]);
184 let base = unimath_nested(doc, &args[1], PREC_MULOP);
185 let op_str = match n.as_str() {
186 "2" => "\u{221A}".to_string(),
187 "3" => "\u{221B}".to_string(),
188 "4" => "\u{221C}".to_string(),
189 _ => format!("\\root {}\\of", n),
190 };
191 (format!("{}{}", op_str, base), PREC_MULOP)
192 } else {
193 unimath_prefix(doc, op, args)
194 }
195 },
196 (_, "continued-fraction") => unimath_error("continued fraction"),
197 ("FRACOP", _) => {
198 if args.len() >= 2 {
199 let thickness = children[0].get_attribute("thickness");
200 if thickness.is_some() {
201 let num = unimath_nested(doc, &args[0], 0);
203 let den = unimath_nested(doc, &args[1], 0);
204 (format!("({}\u{00A6}{})", num, den), PREC_SYMBOL)
205 } else {
206 let num = unimath_nested(doc, &args[0], PREC_MULOP);
207 let den = unimath_nested(doc, &args[1], PREC_MULOP);
208 (format!("{}/{}", num, den), 1)
209 }
210 } else {
211 unimath_prefix(doc, op, args)
212 }
213 },
214 ("SUPERSCRIPTOP", _) => {
215 if args.len() >= 2 {
216 unimath_sup(doc, Some(&args[0]), &args[1])
217 } else {
218 unimath_prefix(doc, op, args)
219 }
220 },
221 ("SUBSCRIPTOP", _) => {
222 if args.len() >= 2 {
223 unimath_sub(doc, Some(&args[0]), &args[1])
224 } else {
225 unimath_prefix(doc, op, args)
226 }
227 },
228 ("OVERACCENT", _) => {
229 if !args.is_empty() {
230 unimath_overaccent(doc, op, &args[0])
231 } else {
232 unimath_prefix(doc, op, args)
233 }
234 },
235 ("UNDERACCENT", _) => {
236 if !args.is_empty() {
237 unimath_underaccent(doc, op, &args[0])
238 } else {
239 unimath_prefix(doc, op, args)
240 }
241 },
242 ("POSTFIX", _) => {
243 if !args.is_empty() {
244 let (op_str, _) = unimath_internal(doc, op);
245 let base = unimath_nested(doc, &args[0], PREC_MULOP);
246 (format!("{}{}", base, op_str), PREC_MULOP)
247 } else {
248 unimath_prefix(doc, op, args)
249 }
250 },
251 ("ENCLOSE", _) => {
252 if !args.is_empty() {
253 (unimath_nested(doc, &args[0], PREC_SYMBOL), PREC_SYMBOL)
254 } else {
255 (String::new(), PREC_SYMBOL)
256 }
257 },
258 _ => {
259 if let Some(prec) = infix_prec(&op_role) {
261 unimath_infix_with_prec(doc, op, args, prec)
262 } else {
263 unimath_prefix(doc, op, args)
265 }
266 },
267 }
268 },
269 "ltx:XMTok" => {
270 let meaning = node.get_attribute("meaning").unwrap_or_default();
271 if meaning == "absent" {
272 return (String::new(), PREC_SYMBOL);
273 }
274 let text = stylize_content(node);
275 (text, PREC_SYMBOL)
276 },
277 "ltx:XMHint" => (String::new(), 0),
278 "ltx:XMArray" => {
279 let rows: Vec<String> = element_children(node)
280 .iter()
281 .map(|row| {
282 element_children(row)
283 .iter()
284 .map(|cell| unimath_nested(doc, cell, 0))
285 .collect::<Vec<_>>()
286 .join("&")
287 })
288 .collect();
289 (format!("\u{25A0}({})", rows.join("@")), 0)
290 },
291 "ltx:XMText" => unimath_text(node),
292 "ltx:XMRef" => match realize(doc, node) {
293 Some(target) => unimath_internal(doc, &target),
294 _ => unimath_error("Unresolved XMRef"),
295 },
296 "ltx:ERROR" => unimath_error(&node.get_content()),
297 _ => unimath_text(node),
298 }
299}
300
301fn unimath_nested(doc: &PostDocument, node: &Node, prec: i32) -> String {
305 let (string, iprec) = unimath_internal(doc, node);
306 if iprec >= prec {
307 string
308 } else {
309 format!("{{{}}}", string)
310 }
311}
312
313fn unimath_map(doc: &PostDocument, args: &[Node]) -> (String, i32) {
317 let mut oprec = 0;
318 if args.len() > 1 {
320 let first_role = args
321 .first()
322 .and_then(|n| n.get_attribute("role"))
323 .unwrap_or_default();
324 let last_role = args
325 .last()
326 .and_then(|n| n.get_attribute("role"))
327 .unwrap_or_default();
328 if first_role == "OPEN" && last_role == "CLOSE" {
329 oprec = PREC_SYMBOL;
330 }
331 }
332 let result: String = args.iter().map(|a| unimath_nested(doc, a, 0)).collect();
333 (result, oprec)
334}
335
336fn unimath_prefix(doc: &PostDocument, op: &Node, args: &[Node]) -> (String, i32) {
340 if args.is_empty() {
341 return (String::new(), PREC_SYMBOL);
342 }
343 let op_str = unimath_nested(doc, op, 0);
344 let args_str: String = args
345 .iter()
346 .map(|a| unimath_nested(doc, a, PREC_SYMBOL))
347 .collect();
348 (format!("{}{}", op_str, args_str), PREC_SYMBOL)
349}
350
351fn unimath_infix_with_prec(
355 doc: &PostDocument,
356 op: &Node,
357 args: &[Node],
358 prec: i32,
359) -> (String, i32) {
360 if args.is_empty() {
361 return (String::new(), PREC_SYMBOL);
362 }
363 let opuni = unimath_nested(doc, op, prec);
364 if args.len() == 1 {
365 let arg = unimath_nested(doc, &args[0], prec);
367 (format!("{}{}", opuni, arg), prec)
368 } else {
369 let mut items = vec![unimath_nested(doc, &args[0], prec)];
370 for arg in &args[1..] {
371 items.push(opuni.clone());
372 items.push(unimath_nested(doc, arg, prec));
373 }
374 (items.join(""), prec)
375 }
376}
377
378fn unimath_sub(doc: &PostDocument, base: Option<&Node>, script: &Node) -> (String, i32) {
382 let ubase = base
383 .map(|b| unimath_nested(doc, b, PREC_SCRIPTOP))
384 .unwrap_or_default();
385 let (uscript, prec) = unimath_internal(doc, script);
386 let uscript = if prec < PREC_SCRIPTOP {
387 format!("{{{}}}", uscript)
388 } else {
389 uscript
390 };
391 (format!("{}_{}", ubase, uscript), PREC_SCRIPTOP)
392}
393
394fn unimath_sup(doc: &PostDocument, base: Option<&Node>, script: &Node) -> (String, i32) {
398 let ubase = base
399 .map(|b| unimath_nested(doc, b, PREC_SCRIPTOP))
400 .unwrap_or_default();
401 let (uscript, prec) = unimath_internal(doc, script);
402 let uscript = if prec < PREC_SCRIPTOP {
403 format!("{{{}}}", uscript)
404 } else {
405 uscript
406 };
407 (format!("{}^{}", ubase, uscript), PREC_SCRIPTOP)
408}
409
410fn unimath_overaccent(doc: &PostDocument, op: &Node, base: &Node) -> (String, i32) {
414 let acc = op.get_content();
415 let combining = overaccent_combining(&acc);
416 let (mut ubase, _) = unimath_internal(doc, base);
417 if ubase.chars().count() > 1 {
418 ubase = format!("({})", ubase);
419 }
420 let accent_str = combining.unwrap_or_else(|| format!("\u{252C}{}", acc));
421 (format!("{}{}", ubase, accent_str), PREC_SCRIPTOP)
422}
423
424fn unimath_underaccent(doc: &PostDocument, op: &Node, base: &Node) -> (String, i32) {
428 let acc = op.get_content();
429 let combining = underaccent_combining(&acc);
430 let (mut ubase, _) = unimath_internal(doc, base);
431 if ubase.chars().count() > 1 {
432 ubase = format!("({})", ubase);
433 }
434 let accent_str = combining.unwrap_or_else(|| format!("\u{252C}{}", acc));
435 (format!("{}{}", ubase, accent_str), PREC_SCRIPTOP)
436}
437
438fn stylize_content(node: &Node) -> String {
442 let role = node
443 .get_attribute("role")
444 .unwrap_or_else(|| "ID".to_string());
445 let text = node.get_content();
446 if text.is_empty() {
447 static DEFAULT_CONTENT: &[(&str, &str)] = &[
449 ("MULOP", "\u{2062}"), ("ADDOP", "\u{2064}"), ("PUNCT", "\u{2063}"), ];
453 for (r, default) in DEFAULT_CONTENT {
454 if role == *r {
455 return default.to_string();
456 }
457 }
458 node
459 .get_attribute("name")
460 .or_else(|| node.get_attribute("meaning"))
461 .unwrap_or(role)
462 } else {
463 text
464 }
465}
466
467fn unimath_text(node: &Node) -> (String, i32) {
471 let text = node.get_content();
472 (format!("\"{}\"", text), PREC_SYMBOL)
473}
474
475fn unimath_error(msg: &str) -> (String, i32) { (format!("\"ERROR {}\"", msg), PREC_SYMBOL) }
477
478fn overaccent_combining(acc: &str) -> Option<String> {
480 let c = acc.chars().next()?;
481 let combining = match c {
482 '^' => '\u{0302}', '\u{02C7}' => '\u{030C}', '~' => '\u{0303}', '\u{0084}' => '\u{0301}', '\u{0060}' => '\u{0300}', '\u{02D9}' => '\u{0307}', '\u{00AB}' => '\u{0308}', '\u{00AF}' => '\u{0304}', '\u{2192}' => '\u{20D7}', '\u{02D8}' => '\u{0306}', 'o' => '\u{030A}', '\u{02DD}' => '\u{030B}', _ => return None,
495 };
496 Some(combining.to_string())
497}
498
499fn underaccent_combining(acc: &str) -> Option<String> {
501 let c = acc.chars().next()?;
502 let combining = match c {
503 '\u{00B8}' => '\u{0327}', '.' => '\u{0323}', '\u{00AF}' => '\u{0331}', '=' | ',' => '\u{0361}', _ => return None,
508 };
509 Some(combining.to_string())
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_stylize_empty_token() {
518 let result = stylize_content_for_role("MULOP", "");
520 assert_eq!(result, "\u{2062}");
521 }
522
523 #[test]
524 fn test_overaccent_combining() {
525 assert_eq!(overaccent_combining("^"), Some("\u{0302}".to_string()));
526 assert_eq!(overaccent_combining("~"), Some("\u{0303}".to_string()));
527 assert_eq!(overaccent_combining("x"), None);
528 }
529
530 fn stylize_content_for_role(role: &str, text: &str) -> String {
532 static DEFAULT_CONTENT: &[(&str, &str)] = &[
533 ("MULOP", "\u{2062}"),
534 ("ADDOP", "\u{2064}"),
535 ("PUNCT", "\u{2063}"),
536 ];
537 if text.is_empty() {
538 for (r, default) in DEFAULT_CONTENT {
539 if role == *r {
540 return default.to_string();
541 }
542 }
543 }
544 text.to_string()
545 }
546}