1#![allow(dead_code)] #![allow(clippy::collapsible_match, clippy::collapsible_if)]
3#![allow(clippy::if_same_then_else)] #![allow(clippy::cloned_ref_to_slice_refs)] #[macro_use]
35pub mod diag;
36
37pub mod document;
39pub mod math_processor;
40pub mod object_db;
41pub mod processor;
42pub mod radix;
43
44pub mod collector;
46pub mod crossref;
47pub mod extract;
48pub mod graphics;
49pub mod graphics_cache; pub mod latex_images;
51pub mod lex_math;
52pub mod make_bibliography;
53pub mod make_index;
54pub mod manifest;
55pub mod math_images;
56pub mod mathml;
57pub mod open_math;
58pub mod pack;
59pub mod picture_images;
60pub mod scan;
61pub mod schema_docs;
62pub mod split;
63pub mod stream_split;
64pub mod svg;
65pub mod tex_math;
66pub mod unicode;
67pub mod unicode_math;
68pub mod writer;
69pub mod xmath;
70pub mod xslt;
71
72#[cfg(test)]
75mod test_env;
76
77use std::sync::LazyLock;
78
79use document::PostDocument;
80use processor::{PostError, Processor};
81
82static POST_AUDIT: LazyLock<bool> = LazyLock::new(|| std::env::var("LATEXML_POST_AUDIT").is_ok());
84
85pub struct Post {
90 pub status: PostStatus,
92}
93
94#[derive(Debug, Default)]
96pub struct PostStatus {
97 pub warning_count: u32,
98 pub error_count: u32,
99 pub fatal_count: u32,
100 pub info_count: u32,
101}
102
103impl Post {
104 pub fn new() -> Self { Post { status: PostStatus::default() } }
106
107 pub fn process_chain(
115 &mut self,
116 docs: Vec<PostDocument>,
117 processors: &mut [Box<dyn Processor>],
118 ) -> Result<Vec<PostDocument>, PostError> {
119 let mut docs = docs;
120
121 Note!("post-processing");
122 let audit = *POST_AUDIT;
123
124 for processor in processors.iter_mut() {
125 let pname = processor.get_name();
131 let phase = if pname.starts_with("MathML[Presentation]") {
132 latexml_core::telemetry::Phase::MathmlPres
133 } else if pname.starts_with("MathML[Content]") {
134 latexml_core::telemetry::Phase::MathmlCont
135 } else if pname.starts_with("XSLT") {
136 latexml_core::telemetry::Phase::Xslt
137 } else if pname.contains("Image") || pname.contains("image") {
138 latexml_core::telemetry::Phase::MathImages
142 } else {
143 latexml_core::telemetry::Phase::Xslt
146 };
147 let _gp = latexml_core::telemetry::phase(phase);
148 let mut new_docs = Vec::new();
149 for doc in docs {
150 let nodes = processor.to_process(&doc);
151 if !nodes.is_empty() {
152 let n = nodes.len();
153 let msg = format!(
154 "{} {} {}",
155 processor.get_name(),
156 doc.site_relative_destination().unwrap_or_default(),
157 if n > 1 {
158 format!("{} to process", n)
159 } else {
160 "processing".to_string()
161 }
162 );
163 Note!(msg);
164 let t0 = if audit {
165 Some(std::time::Instant::now())
166 } else {
167 None
168 };
169 let result_docs = processor.process(doc, nodes)?;
170 if let Some(t0) = t0 {
171 let ms = t0.elapsed().as_millis();
172 Note!(format!(
173 "POST_AUDIT stage {} took {}ms ({} nodes)",
174 processor.get_name(),
175 ms,
176 n
177 ));
178 }
179 new_docs.extend(result_docs);
180 } else {
181 new_docs.push(doc);
182 }
183 }
184 docs = new_docs;
185 }
186
187 for doc in docs.iter_mut() {
196 doc.drain_pending_xmath_unlinks();
197 }
198
199 Note!("post-processing complete");
200 Ok(docs)
201 }
202}
203
204impl Default for Post {
205 fn default() -> Self { Self::new() }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::{
212 document::PostDocumentOptions,
213 writer::{OutputFormat, Writer},
214 };
215
216 #[test]
217 fn test_empty_pipeline() {
218 let mut post = Post::new();
219 let doc = PostDocument::new_from_string(
220 "<document xmlns='http://dlmf.nist.gov/LaTeXML'/>",
221 PostDocumentOptions::default(),
222 )
223 .unwrap();
224
225 let mut processors: Vec<Box<dyn Processor>> = vec![];
226 let result = post.process_chain(vec![doc], &mut processors);
227 assert!(result.is_ok());
228 assert_eq!(result.unwrap().len(), 1);
229 }
230
231 #[test]
232 fn test_writer_pipeline() {
233 let mut post = Post::new();
234 let doc = PostDocument::new_from_string(
235 "<document xmlns='http://dlmf.nist.gov/LaTeXML'><title>Test</title></document>",
236 PostDocumentOptions::default(),
237 )
238 .unwrap();
239
240 let writer = Writer::new(Some(OutputFormat::Xml), false, false);
242 let mut processors: Vec<Box<dyn Processor>> = vec![Box::new(writer)];
243 let result = post.process_chain(vec![doc], &mut processors);
244 assert!(result.is_ok());
245 }
246
247 #[test]
248 fn test_pmml_pipeline() {
249 let mut post = Post::new();
250 let doc = PostDocument::new_from_string(
251 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
252 <para xml:id='p1'><p>Inline <Math mode='inline' tex='a+b' text='a + b' xml:id='p1.m1'>\
253 <XMath><XMApp>\
254 <XMTok meaning='plus' role='ADDOP'>+</XMTok>\
255 <XMTok font='italic' role='ID'>a</XMTok>\
256 <XMTok font='italic' role='ID'>b</XMTok>\
257 </XMApp></XMath></Math></p></para>\
258 </document>",
259 PostDocumentOptions::default(),
260 )
261 .unwrap();
262
263 let pmml = mathml::MathML::new_presentation().with_keep_xmath(true);
264 let mut processors: Vec<Box<dyn Processor>> = vec![Box::new(pmml)];
265 let result = post.process_chain(vec![doc], &mut processors);
266 assert!(result.is_ok());
267 let docs = result.unwrap();
268 let output = docs[0].to_xml_string();
269 eprintln!("PMML output:\n{}", output);
270 assert!(
272 output.contains("<XMath>") || output.contains("<XMath "),
273 "XMath should be preserved"
274 );
275 assert!(
276 output.contains("m:math"),
277 "m:math element should be present"
278 );
279 assert!(output.contains("m:mi"), "m:mi element should be present");
280 assert!(output.contains("m:mo"), "m:mo element should be present");
281 }
282
283 #[test]
291 fn test_parallel_pmml_cmml_combine_parallel() {
292 let mut post = Post::new();
293 let doc = PostDocument::new_from_string(
294 "<document xmlns='http://dlmf.nist.gov/LaTeXML'>\
295 <para xml:id='p1'><p>Inline <Math mode='inline' tex='a+b' text='a + b' xml:id='p1.m1'>\
296 <XMath><XMApp>\
297 <XMTok meaning='plus' role='ADDOP'>+</XMTok>\
298 <XMTok font='italic' role='ID'>a</XMTok>\
299 <XMTok font='italic' role='ID'>b</XMTok>\
300 </XMApp></XMath></Math></p></para>\
301 </document>",
302 PostDocumentOptions::default(),
303 )
304 .unwrap();
305
306 let content = mathml::MathML::new_content().secondary();
309 let pmml = mathml::MathML::new_presentation()
310 .with_mathtex(true)
311 .with_secondaries(vec![Box::new(content)]);
312 let mut processors: Vec<Box<dyn Processor>> = vec![Box::new(pmml)];
313 let result = post.process_chain(vec![doc], &mut processors);
314 assert!(result.is_ok());
315 let docs = result.unwrap();
316 let output = docs[0].to_xml_string();
317 eprintln!("parallel P+C output:\n{output}");
318
319 assert!(
321 output.contains("m:semantics"),
322 "m:semantics wrapper missing"
323 );
324 assert!(
325 output.contains("m:apply"),
326 "content m:apply should be generated"
327 );
328 assert!(
329 output.contains(r#"m:annotation-xml encoding="MathML-Content""#),
330 "content must be folded into <m:annotation-xml encoding=\"MathML-Content\">"
331 );
332 assert!(
333 output.contains(r#"encoding="application/x-tex""#),
334 "x-tex source annotation should be present in the parallel semantics"
335 );
336 let after_semantics = output.rsplit("</m:semantics>").next().unwrap_or("");
339 assert!(
340 !after_semantics.contains("m:apply") && !after_semantics.contains("m:ci"),
341 "orphan Content MathML found after </m:semantics> — combine_parallel not applied:\n{output}"
342 );
343 }
344
345 #[test]
346 fn test_scan_pipeline() {
347 let mut post = Post::new();
348 let doc = PostDocument::new_from_string(
349 "<document xmlns='http://dlmf.nist.gov/LaTeXML' xml:id='doc'>\
350 <section xml:id='s1'><title>First</title></section>\
351 <section xml:id='s2'><title>Second</title></section>\
352 </document>",
353 PostDocumentOptions::default(),
354 )
355 .unwrap();
356
357 let db = object_db::ObjectDB::new();
358 let scanner = scan::Scan::new(db);
359 let mut processors: Vec<Box<dyn Processor>> = vec![Box::new(scanner)];
360 let result = post.process_chain(vec![doc], &mut processors);
361 assert!(result.is_ok());
362 }
363}