latexml_post/
processor.rs1use std::path::PathBuf;
7
8use libxml::tree::Node;
9use regex::Regex;
10use rustc_hash::FxHashMap as HashMap;
11
12use crate::document::PostDocument;
13
14#[derive(Debug, Default, Clone)]
16pub struct ProcessorOptions {
17 pub resource_directory: Option<String>,
18 pub resource_prefix: Option<String>,
19}
20
21pub type ProcessResult = Result<Vec<PostDocument>, PostError>;
23
24#[derive(Debug)]
26pub enum PostError {
27 Processing(String),
29 Io(std::io::Error),
31 Xml(String),
33}
34
35impl std::fmt::Display for PostError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 PostError::Processing(msg) => write!(f, "Post-processing error: {}", msg),
39 PostError::Io(err) => write!(f, "I/O error: {}", err),
40 PostError::Xml(msg) => write!(f, "XML error: {}", msg),
41 }
42 }
43}
44
45impl std::error::Error for PostError {}
46
47impl From<std::io::Error> for PostError {
48 fn from(err: std::io::Error) -> Self { PostError::Io(err) }
49}
50
51pub trait Processor {
57 fn get_name(&self) -> &str;
59
60 fn resource_directory(&self) -> Option<&str> { None }
62
63 fn resource_prefix(&self) -> Option<&str> { None }
65
66 fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
70 match doc.get_document_element() {
71 Some(el) => vec![el],
72 None => vec![],
73 }
74 }
75
76 fn process(&mut self, doc: PostDocument, nodes: Vec<Node>) -> ProcessResult;
79
80 fn desired_resource_pathname(
82 &self,
83 _doc: &PostDocument,
84 _node: &Node,
85 _source: Option<&str>,
86 _type_ext: Option<&str>,
87 ) -> Option<PathBuf> {
88 None
89 }
90
91 fn generate_resource_pathname(
93 &self,
94 doc: &mut PostDocument,
95 _node: &Node,
96 _source: Option<&str>,
97 type_ext: Option<&str>,
98 ) -> PathBuf {
99 let subdir = self.resource_directory().unwrap_or("");
100 let prefix = self.resource_prefix().unwrap_or("x");
101 let counter_key = format!("_max_{}_{}_counter_", subdir, prefix);
102 let n = doc
103 .cache_lookup(&counter_key)
104 .and_then(|v| v.parse::<u32>().ok())
105 .unwrap_or(0)
106 + 1;
107 doc.cache_store(&counter_key, &n.to_string());
108 let name = format!("{}{}", prefix, n);
109 let mut path = PathBuf::from(subdir);
110 let filename = if let Some(ext) = type_ext {
111 format!("{}.{}", name, ext)
112 } else {
113 name
114 };
115 path.push(filename);
116 path
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct ClassInfo {
123 pub name: String,
124 pub options: String,
125 pub oldstyle: Option<String>,
126}
127
128#[derive(Debug, Clone)]
130pub struct PackageInfo {
131 pub name: String,
132 pub options: String,
133}
134
135pub fn find_documentclass_and_packages(doc: &PostDocument) -> (ClassInfo, Vec<PackageInfo>) {
141 let pi_re = Regex::new(r#"\s*([\w\-_]*)=[\"'](.*?)[\"']"#).unwrap();
142 let mut class: Option<String> = None;
143 let mut classoptions = String::from("onecolumn");
144 let mut oldstyle: Option<String> = None;
145 let mut packages = Vec::new();
146
147 for pi in doc.findnodes("//processing-instruction('latexml')") {
148 let data = pi.get_content();
149 let mut entry = HashMap::default();
150 for cap in pi_re.captures_iter(&data) {
151 entry.insert(cap[1].to_string(), cap[2].to_string());
152 }
153 if let Some(cls) = entry.get("class") {
154 class = Some(cls.clone());
155 classoptions = entry
156 .get("options")
157 .cloned()
158 .unwrap_or_else(|| "onecolumn".to_string());
159 oldstyle = entry.get("oldstyle").cloned();
160 } else if let Some(pkg) = entry.get("package") {
161 let opts = entry.get("options").cloned().unwrap_or_default();
162 for p in pkg.split(',').map(str::trim).filter(|s| !s.is_empty()) {
163 packages.push(PackageInfo {
164 name: p.to_string(),
165 options: opts.clone(),
166 });
167 }
168 }
169 }
170
171 if class.is_none() {
172 Warn!(
175 "expected",
176 "class",
177 "No document class found; using article"
178 );
179 }
180
181 let class_info = ClassInfo {
182 name: class.unwrap_or_else(|| "article".to_string()),
183 options: classoptions,
184 oldstyle,
185 };
186 (class_info, packages)
187}
188
189pub fn find_preambles(doc: &PostDocument) -> String {
193 let pi_re = Regex::new(r#"\s*([\w\-_]*)=[\"'](.*?)[\"']"#).unwrap();
194 let mut preambles = Vec::new();
195
196 for pi in doc.findnodes("//processing-instruction('latexml')") {
197 let data = pi.get_content();
198 for cap in pi_re.captures_iter(&data) {
199 if &cap[1] == "preamble" {
200 preambles.push(cap[2].to_string());
201 }
202 }
203 }
204
205 preambles.join("\n")
206}
207
208pub fn copy_foreign_attributes(target: &mut Node, source: &Node) {
215 let props = source.get_properties();
216 for (key, value) in &props {
217 if key.starts_with("xml:") {
218 continue;
219 }
220 if !key.contains(':') {
221 continue;
222 }
223 if target.get_attribute(key).is_none() {
225 target.set_attribute(key, value).ok();
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn processor_options_default_is_empty() {
236 let o = ProcessorOptions::default();
237 assert!(o.resource_directory.is_none());
238 assert!(o.resource_prefix.is_none());
239 }
240
241 #[test]
242 fn processor_options_clone_preserves() {
243 let o = ProcessorOptions {
244 resource_directory: Some("/tmp".to_string()),
245 resource_prefix: Some("pre".to_string()),
246 };
247 let c = o.clone();
248 assert_eq!(c.resource_directory, Some("/tmp".to_string()));
249 assert_eq!(c.resource_prefix, Some("pre".to_string()));
250 }
251
252 #[test]
253 fn post_error_display_processing() {
254 let e = PostError::Processing("boom".to_string());
255 let s = format!("{e}");
256 assert!(s.contains("Post-processing error"));
257 assert!(s.contains("boom"));
258 }
259
260 #[test]
261 fn post_error_display_xml() {
262 let e = PostError::Xml("malformed".to_string());
263 let s = format!("{e}");
264 assert!(s.contains("XML error"));
265 assert!(s.contains("malformed"));
266 }
267
268 #[test]
269 fn post_error_from_io_error() {
270 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
271 let pe: PostError = io.into();
272 match pe {
273 PostError::Io(_) => {},
274 other => panic!("expected Io, got {other:?}"),
275 }
276 }
277
278 #[test]
279 fn post_error_display_io() {
280 let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
281 let e = PostError::Io(io);
282 let s = format!("{e}");
283 assert!(s.contains("I/O error"));
284 }
285
286 #[test]
287 fn post_error_impls_std_error() {
288 fn take_err<E: std::error::Error>(_: &E) {}
290 let e = PostError::Processing("x".to_string());
291 take_err(&e);
292 }
293}