use easydoc_math::latex_to_omml;
const OMML_NS: &str = "xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\"";
#[must_use]
pub fn postprocess_math_xml(document_xml: &str, math: &[(String, String, bool)]) -> String {
let mut xml = ensure_omml_namespace(document_xml);
for (marker, latex, display) in math {
let replacement = match latex_to_omml::convert(latex) {
Ok(omml) if !omml.trim().is_empty() && !is_empty_omath(&omml) => {
build_omath_paragraph(&omml, *display)
}
_ => build_latex_fallback_paragraph(latex, *display),
};
if let Some((para_start, para_end)) = find_containing_paragraph(&xml, marker) {
xml.replace_range(para_start..para_end, &replacement);
} else {
xml = xml.replace(marker, &replacement);
}
}
xml
}
fn ensure_omml_namespace(document_xml: &str) -> String {
if document_xml.contains("xmlns:m=") {
return document_xml.to_owned();
}
let Some(start) = document_xml.find("<w:document") else {
return document_xml.to_owned();
};
let Some(tag_end) = document_xml[start..].find('>') else {
return document_xml.to_owned();
};
let abs_end = start + tag_end;
let mut xml = String::with_capacity(document_xml.len() + OMML_NS.len() + 1);
xml.push_str(&document_xml[..abs_end]);
xml.push(' ');
xml.push_str(OMML_NS);
xml.push_str(&document_xml[abs_end..]);
xml
}
fn is_empty_omath(omml: &str) -> bool {
let inner = omml
.trim()
.strip_prefix("<m:oMath>")
.and_then(|s| s.strip_suffix("</m:oMath>"))
.unwrap_or(omml.trim());
inner.trim().is_empty()
}
fn build_omath_paragraph(omml: &str, display: bool) -> String {
if display {
format!(
"<w:p><m:oMathPara><m:oMathParaPr><m:jc m:val=\"center\"/></m:oMathParaPr>\
{omml}</m:oMathPara></w:p>"
)
} else {
format!("<w:p>{omml}</w:p>")
}
}
fn build_latex_fallback_paragraph(latex: &str, display: bool) -> String {
let latex_text = if display {
format!("$${latex}$$")
} else {
format!("${latex}$")
};
format!(
"<w:p><w:r><w:rPr><w:rFonts w:ascii=\"Courier New\"/></w:rPr>\
<w:t xml:space=\"preserve\">{latex_text}</w:t></w:r></w:p>"
)
}
fn find_containing_paragraph(xml: &str, marker: &str) -> Option<(usize, usize)> {
let marker_pos = xml.find(marker)?;
let before = &xml[..marker_pos];
let mut para_start = None;
let mut search = 0;
while let Some(rel) = before[search..].find("<w:p") {
let abs = search + rel;
let after = before[abs + 4..].chars().next();
if matches!(after, Some('>' | ' ' | '/')) {
para_start = Some(abs);
}
search = abs + 4;
}
let para_start = para_start?;
let after_marker = &xml[marker_pos..];
let close_rel = after_marker.find("</w:p>")?;
let para_end = marker_pos + close_rel + "</w:p>".len();
Some((para_start, para_end))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inline_math_injects_omath() {
let xml = "<w:document><w:body><w:p><w:r><w:t>@@EASYDOC_MATH_0@@</w:t></w:r></w:p></w:body></w:document>";
let math = vec![("@@EASYDOC_MATH_0@@".to_string(), "x^2".to_string(), false)];
let out = postprocess_math_xml(xml, &math);
assert!(out.contains("<m:oMath>"), "{out}");
assert!(out.contains("<m:sSup>"), "{out}");
assert!(out.contains("xmlns:m="), "应注入命名空间:{out}");
assert!(!out.contains("@@EASYDOC_MATH"), "{out}");
}
#[test]
fn display_math_wraps_omath_para() {
let xml =
"<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
let math = vec![("@@M@@".to_string(), r"\frac{a}{b}".to_string(), true)];
let out = postprocess_math_xml(xml, &math);
assert!(out.contains("<m:oMathPara>"), "{out}");
assert!(out.contains("<m:jc m:val=\"center\"/>"), "{out}");
assert!(out.contains("<m:oMath>"), "{out}");
}
#[test]
fn unsupported_latex_falls_back_to_source() {
let xml =
"<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
let math = vec![("@@M@@".to_string(), r"\cancel{x}".to_string(), true)];
let out = postprocess_math_xml(xml, &math);
assert!(out.contains(r"$$\cancel{x}$$"), "应保留 LaTeX 原文:{out}");
assert!(!out.contains("<m:oMath>"), "{out}");
}
#[test]
fn empty_omath_falls_back() {
let xml =
"<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
let math = vec![("@@M@@".to_string(), " ".to_string(), false)];
let out = postprocess_math_xml(xml, &math);
assert!(out.contains("$ $"), "{out}");
}
#[test]
fn namespace_not_duplicated() {
let xml = "<w:document xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\"><w:body></w:body></w:document>";
let out = ensure_omml_namespace(xml);
assert_eq!(out.matches("xmlns:m=").count(), 1, "{out}");
}
#[test]
fn namespace_injected_into_root_tag() {
let xml = "<w:document xmlns:w=\"http://x\"><w:body></w:body></w:document>";
let out = ensure_omml_namespace(xml);
assert!(
out.starts_with("<w:document xmlns:w=\"http://x\" xmlns:m="),
"{out}"
);
}
}