easydoc_writer/
math_omml.rs1use easydoc_math::latex_to_omml;
11
12const OMML_NS: &str = "xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\"";
14
15#[must_use]
35pub fn postprocess_math_xml(document_xml: &str, math: &[(String, String, bool)]) -> String {
36 let mut xml = ensure_omml_namespace(document_xml);
37 for (marker, latex, display) in math {
38 let replacement = match latex_to_omml::convert(latex) {
39 Ok(omml) if !omml.trim().is_empty() && !is_empty_omath(&omml) => {
40 build_omath_paragraph(&omml, *display)
41 }
42 _ => build_latex_fallback_paragraph(latex, *display),
44 };
45 if let Some((para_start, para_end)) = find_containing_paragraph(&xml, marker) {
48 xml.replace_range(para_start..para_end, &replacement);
49 } else {
50 xml = xml.replace(marker, &replacement);
51 }
52 }
53 xml
54}
55
56fn ensure_omml_namespace(document_xml: &str) -> String {
61 if document_xml.contains("xmlns:m=") {
62 return document_xml.to_owned();
63 }
64 let Some(start) = document_xml.find("<w:document") else {
65 return document_xml.to_owned();
66 };
67 let Some(tag_end) = document_xml[start..].find('>') else {
68 return document_xml.to_owned();
69 };
70 let abs_end = start + tag_end;
71 let mut xml = String::with_capacity(document_xml.len() + OMML_NS.len() + 1);
72 xml.push_str(&document_xml[..abs_end]);
73 xml.push(' ');
74 xml.push_str(OMML_NS);
75 xml.push_str(&document_xml[abs_end..]);
76 xml
77}
78
79fn is_empty_omath(omml: &str) -> bool {
81 let inner = omml
82 .trim()
83 .strip_prefix("<m:oMath>")
84 .and_then(|s| s.strip_suffix("</m:oMath>"))
85 .unwrap_or(omml.trim());
86 inner.trim().is_empty()
87}
88
89fn build_omath_paragraph(omml: &str, display: bool) -> String {
93 if display {
94 format!(
95 "<w:p><m:oMathPara><m:oMathParaPr><m:jc m:val=\"center\"/></m:oMathParaPr>\
96 {omml}</m:oMathPara></w:p>"
97 )
98 } else {
99 format!("<w:p>{omml}</w:p>")
100 }
101}
102
103fn build_latex_fallback_paragraph(latex: &str, display: bool) -> String {
105 let latex_text = if display {
106 format!("$${latex}$$")
107 } else {
108 format!("${latex}$")
109 };
110 format!(
111 "<w:p><w:r><w:rPr><w:rFonts w:ascii=\"Courier New\"/></w:rPr>\
112 <w:t xml:space=\"preserve\">{latex_text}</w:t></w:r></w:p>"
113 )
114}
115
116fn find_containing_paragraph(xml: &str, marker: &str) -> Option<(usize, usize)> {
121 let marker_pos = xml.find(marker)?;
122 let before = &xml[..marker_pos];
124 let mut para_start = None;
125 let mut search = 0;
126 while let Some(rel) = before[search..].find("<w:p") {
127 let abs = search + rel;
128 let after = before[abs + 4..].chars().next();
129 if matches!(after, Some('>' | ' ' | '/')) {
130 para_start = Some(abs);
131 }
132 search = abs + 4;
133 }
134 let para_start = para_start?;
135 let after_marker = &xml[marker_pos..];
137 let close_rel = after_marker.find("</w:p>")?;
138 let para_end = marker_pos + close_rel + "</w:p>".len();
139 Some((para_start, para_end))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn inline_math_injects_omath() {
148 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>";
149 let math = vec![("@@EASYDOC_MATH_0@@".to_string(), "x^2".to_string(), false)];
150 let out = postprocess_math_xml(xml, &math);
151 assert!(out.contains("<m:oMath>"), "{out}");
152 assert!(out.contains("<m:sSup>"), "{out}");
153 assert!(out.contains("xmlns:m="), "应注入命名空间:{out}");
154 assert!(!out.contains("@@EASYDOC_MATH"), "{out}");
155 }
156
157 #[test]
158 fn display_math_wraps_omath_para() {
159 let xml =
160 "<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
161 let math = vec![("@@M@@".to_string(), r"\frac{a}{b}".to_string(), true)];
162 let out = postprocess_math_xml(xml, &math);
163 assert!(out.contains("<m:oMathPara>"), "{out}");
164 assert!(out.contains("<m:jc m:val=\"center\"/>"), "{out}");
165 assert!(out.contains("<m:oMath>"), "{out}");
166 }
167
168 #[test]
169 fn unsupported_latex_falls_back_to_source() {
170 let xml =
171 "<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
172 let math = vec![("@@M@@".to_string(), r"\cancel{x}".to_string(), true)];
173 let out = postprocess_math_xml(xml, &math);
174 assert!(out.contains(r"$$\cancel{x}$$"), "应保留 LaTeX 原文:{out}");
175 assert!(!out.contains("<m:oMath>"), "{out}");
176 }
177
178 #[test]
179 fn empty_omath_falls_back() {
180 let xml =
181 "<w:document><w:body><w:p><w:r><w:t>@@M@@</w:t></w:r></w:p></w:body></w:document>";
182 let math = vec![("@@M@@".to_string(), " ".to_string(), false)];
183 let out = postprocess_math_xml(xml, &math);
184 assert!(out.contains("$ $"), "{out}");
185 }
186
187 #[test]
188 fn namespace_not_duplicated() {
189 let xml = "<w:document xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\"><w:body></w:body></w:document>";
190 let out = ensure_omml_namespace(xml);
191 assert_eq!(out.matches("xmlns:m=").count(), 1, "{out}");
192 }
193
194 #[test]
195 fn namespace_injected_into_root_tag() {
196 let xml = "<w:document xmlns:w=\"http://x\"><w:body></w:body></w:document>";
197 let out = ensure_omml_namespace(xml);
198 assert!(
199 out.starts_with("<w:document xmlns:w=\"http://x\" xmlns:m="),
200 "{out}"
201 );
202 }
203}