diff --git a/crates/formats/src/mathml.rs b/crates/formats/src/mathml.rs index cf5d8fa3e..d9974b1fc 100644 --- a/crates/formats/src/mathml.rs +++ b/crates/formats/src/mathml.rs @@ -175,8 +175,16 @@ fn node(n: &MNode, o: &mut String, depth: usize, eq_arr: bool) { o.push_str(""); group(base, o, depth); o.push_str(""); - group(sub, o, depth); - group(sup, o, depth); + if sub.is_empty() { + o.push_str(""); + } else { + group(sub, o, depth); + } + if sup.is_empty() { + o.push_str(""); + } else { + group(sup, o, depth); + } o.push_str(""); } }, @@ -443,7 +451,8 @@ pub fn local(name: &str) -> &str { /// The linear format of a `` element, and whether it's a display equation. `None` when it /// holds nothing. pub fn read(math: &El) -> Option<(String, bool)> { - let display = math.attr("display").is_some_and(|d| d.eq_ignore_ascii_case("block")) || math.attr("mode").is_some_and(|d| d == "display"); + let display = + math.attr("display").is_some_and(|d| d.eq_ignore_ascii_case("block")) || math.attr("mode").is_some_and(|d| d.eq_ignore_ascii_case("display")); let linear = linear_of(math); let linear: String = linear.chars().filter(|c| !c.is_control()).collect(); let linear = linear.trim().to_string(); @@ -636,7 +645,20 @@ fn elem(e: &El, depth: usize, budget: &mut usize) -> Arg { Some(p) => { let sub = script(p + 1, budget); let sup = script(p + 2, budget); - vec![MNode::Script { kind: ScriptKind::Pre, base, sub, sup }] + let pre_node = MNode::Script { kind: ScriptKind::Pre, base, sub, sup }; + if p > 1 { + let post_sub = script(1, budget); + let post_sup = script(2, budget); + let kind = match (post_sub.is_empty(), post_sup.is_empty()) { + (true, true) => return vec![pre_node], + (false, true) => ScriptKind::Sub, + (true, false) => ScriptKind::Sup, + (false, false) => ScriptKind::SubSup, + }; + vec![MNode::Script { kind, base: vec![pre_node], sub: post_sub, sup: post_sup }] + } else { + vec![pre_node] + } } None => { let sub = script(1, budget); @@ -660,12 +682,9 @@ fn elem(e: &El, depth: usize, budget: &mut usize) -> Arg { vec![MNode::Delim { beg, end, sep, grow: true, shp_match: false, elems }] } "mtable" => { - let rows: Vec> = e - .els() - .filter(|r| matches!(local(&r.name), "mtr" | "mlabeledtr")) - .take(256) - .map(|r| r.els().filter(|c| local(&c.name) == "mtd").take(256).map(|c| children(c, d, budget)).collect()) - .collect(); + let mtr_list: Vec<&El> = e.els().filter(|r| matches!(local(&r.name), "mtr" | "mlabeledtr")).take(256).collect(); + let rows: Vec> = + mtr_list.iter().map(|r| r.els().filter(|c| local(&c.name) == "mtd").take(256).map(|c| children(c, d, budget)).collect()).collect(); if rows.is_empty() { return Vec::new(); } @@ -676,14 +695,68 @@ fn elem(e: &El, depth: usize, budget: &mut usize) -> Arg { if marked && rows.iter().all(|r| r.len() <= 1) { return vec![MNode::EqArr { rows: rows.into_iter().map(|mut r| r.pop().unwrap_or_default()).collect() }]; } - vec![MNode::Matrix { rows, col_jc: Vec::new() }] + let mut col_jc: Vec = e + .attr("columnalign") + .map(|ca| { + ca.split_whitespace() + .map(|a| match a { + "left" => ColJc::Left, + "right" => ColJc::Right, + _ => ColJc::Center, + }) + .collect() + }) + .unwrap_or_default(); + if col_jc.is_empty() + && let Some(first_row) = mtr_list.first() + { + let cell_aligns: Vec = first_row + .els() + .filter(|c| local(&c.name) == "mtd") + .map(|c| match c.attr("columnalign") { + Some("left") => ColJc::Left, + Some("right") => ColJc::Right, + _ => ColJc::Center, + }) + .collect(); + if cell_aligns.iter().any(|a| *a != ColJc::Center) { + col_jc = cell_aligns; + } + } + vec![MNode::Matrix { rows, col_jc }] + } + "mpadded" => { + let zero_wid = e.attr("width").is_some_and(|w| w.trim() == "0"); + let zero_asc = e.attr("height").is_some_and(|h| h.trim() == "0"); + let zero_desc = e.attr("depth").is_some_and(|d| d.trim() == "0"); + let ch = children(e, d, budget); + if zero_wid || zero_asc || zero_desc { + if let [MNode::Phant { show, e, .. }] = ch.as_slice() { + vec![MNode::Phant { show: *show, zero_wid, zero_asc, zero_desc, e: e.clone() }] + } else { + vec![MNode::Phant { show: true, zero_wid, zero_asc, zero_desc, e: ch }] + } + } else { + ch + } } "mphantom" => vec![MNode::Phant { show: false, zero_wid: false, zero_asc: false, zero_desc: false, e: children(e, d, budget) }], "menclose" => { let n = e.attr("notation").unwrap_or("longdiv"); let has = |k: &str| n.split_whitespace().any(|w| w == k); - if has("box") || has("roundedbox") { - return vec![MNode::BorderBox { hide: [false; 4], strike: [false; 4], e: children(e, d, budget) }]; + let is_box = has("box") || has("roundedbox") || has("circle"); + let has_border = is_box || has("top") || has("bottom") || has("left") || has("right"); + let has_strike = has("horizontalstrike") || has("verticalstrike") || has("updiagonalstrike") || has("downdiagonalstrike"); + if has_border || has_strike { + let hide = if is_box { + [false; 4] + } else if has_border { + [!has("top"), !has("bottom"), !has("left"), !has("right")] + } else { + [true; 4] + }; + let strike = [has("horizontalstrike"), has("verticalstrike"), has("updiagonalstrike"), has("downdiagonalstrike")]; + return vec![MNode::BorderBox { hide, strike, e: children(e, d, budget) }]; } children(e, d, budget) } @@ -751,7 +824,7 @@ mod tests { let gc = to_mathml(&[MNode::GroupChr { chr: '⏟', top: false, e: parse_linear("a") }], false, ""); assert!(gc.contains("a⏟"), "{gc}"); let pre = to_mathml(&[MNode::Script { kind: ScriptKind::Pre, base: parse_linear("C"), sub: parse_linear("n"), sup: vec![] }], false, ""); - assert!(pre.contains(""), "{pre}"); + assert!(pre.contains("") && pre.contains(""), "{pre}"); let eq = to_mathml(&[MNode::EqArr { rows: vec![parse_linear("x&=1"), parse_linear("y&=2")] }], false, ""); assert!(eq.contains("x=1") && !eq.contains("&"), "{eq}"); let ph = to_mathml(&[MNode::Phant { show: false, zero_wid: true, zero_asc: false, zero_desc: false, e: parse_linear("a") }], false, ""); @@ -884,6 +957,25 @@ mod tests { assert!(matches!(t.as_slice(), [MNode::EqArr { .. }]), "{ml}: {t:?}"); } + #[test] + fn mmultiscripts_and_borderbox_round_trip() { + let html = "X12"; + let p = pasted(html); + assert!(!p.is_empty(), "should parse mmultiscripts with pre and post scripts"); + + let html_box = "x"; + let p_box = pasted(html_box); + assert!(!p_box.is_empty(), "should parse menclose"); + + let html_tbl = "ab"; + let p_tbl = pasted(html_tbl); + assert!(!p_tbl.is_empty(), "should parse mtable with columnalign"); + + let html_pad = "z"; + let p_pad = pasted(html_pad); + assert!(!p_pad.is_empty(), "should parse mpadded phantom"); + } + #[test] fn deep_nesting_is_bounded() { let mut n = parse_linear("x");