latexml_post/
collector.rs1use std::path::Path;
9
10use libxml::tree::Node;
11use rustc_hash::FxHashMap as HashMap;
12
13use crate::{
14 document::{NodeData, PostDocument, get_xml_id},
15 processor::{ProcessResult, Processor},
16};
17
18pub struct Collector {
23 name: String,
24 resource_directory: Option<String>,
25 resource_prefix: Option<String>,
26 has_scanner: bool,
29}
30
31impl Collector {
32 pub fn new(name: &str) -> Self {
33 Collector {
34 name: name.to_string(),
35 resource_directory: None,
36 resource_prefix: None,
37 has_scanner: false,
38 }
39 }
40
41 pub fn with_scanner(mut self) -> Self {
43 self.has_scanner = true;
44 self
45 }
46}
47
48pub fn make_sub_collection_documents(
58 doc: &mut PostDocument,
59 root: &Node,
60 collections: &HashMap<String, Vec<NodeData>>,
61) -> Vec<PostDocument> {
62 let mut initials: Vec<&String> = collections.keys().collect();
63 initials.sort();
64
65 if initials.is_empty() {
66 return vec![];
67 }
68
69 let _root_tag = doc
70 .get_qname(root)
71 .unwrap_or_else(|| "ltx:index".to_string());
72 let root_id = get_xml_id(root).unwrap_or_default();
75
76 let ids: Vec<(String, &str)> = initials
78 .iter()
79 .enumerate()
80 .map(|(i, init)| {
81 if i == 0 {
82 (root_id.clone(), init.as_str())
83 } else {
84 (format!("{}.{}", root_id, init), init.as_str())
85 }
86 })
87 .collect();
88
89 if let Some(first_init) = initials.first() {
91 if let Some(data) = collections.get(*first_init) {
92 let toc_entries: Vec<NodeData> = ids
94 .iter()
95 .enumerate()
96 .map(|(i, (id, init))| {
97 if i == 0 {
98 NodeData::Element {
99 tag: "ltx:tocentry".to_string(),
100 attributes: None,
101 children: vec![NodeData::Text(init.to_string())],
102 }
103 } else {
104 NodeData::Element {
105 tag: "ltx:tocentry".to_string(),
106 attributes: None,
107 children: vec![NodeData::Element {
108 tag: "ltx:ref".to_string(),
109 attributes: Some(HashMap::from_iter([
110 ("idref".to_string(), id.clone()),
111 ("show".to_string(), "refnum".to_string()),
112 ])),
113 children: vec![NodeData::Text(init.to_string())],
114 }],
115 }
116 }
117 })
118 .collect();
119
120 let toc = NodeData::Element {
121 tag: "ltx:TOC".to_string(),
122 attributes: Some(HashMap::from_iter([(
123 "format".to_string(),
124 "veryshort".to_string(),
125 )])),
126 children: vec![NodeData::Element {
127 tag: "ltx:toclist".to_string(),
128 attributes: None,
129 children: toc_entries,
130 }],
131 };
132
133 let mut root_mut = root.clone();
134 let mut content = vec![toc];
135 content.extend(data.clone());
136 doc.add_nodes(&mut root_mut, &content);
137 }
138 }
139
140 Info!(
143 "collector",
144 "subcollections",
145 "Collector: {} sub-collections by initial: {:?}",
146 initials.len(),
147 initials
148 );
149
150 vec![]
153}
154
155pub fn get_page_name(doc: &PostDocument, initial: &str) -> String {
162 if let Some(dest) = doc.get_destination() {
163 let path = Path::new(dest);
164 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("doc");
165 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("xml");
166 let dir = path.parent().and_then(|p| p.to_str()).unwrap_or(".");
167 let name = if stem == "index" {
168 initial.to_string()
169 } else {
170 format!("{}.{}", stem, initial)
171 };
172 format!("{}/{}.{}", dir, name, ext)
173 } else {
174 format!("{}.xml", initial)
175 }
176}
177
178impl Processor for Collector {
179 fn get_name(&self) -> &str { &self.name }
180
181 fn process(&mut self, doc: PostDocument, _nodes: Vec<Node>) -> ProcessResult {
182 Warn!(
186 "misdefined",
187 "Collector",
188 "Abstract Collector::process called — concrete subclass should override"
189 );
190 Ok(vec![doc])
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::document::PostDocumentOptions;
198
199 fn doc_with_dest(dest: Option<&str>) -> PostDocument {
200 let opts = PostDocumentOptions {
201 destination: dest.map(|s| s.to_string()),
202 ..PostDocumentOptions::default()
203 };
204 PostDocument::new_from_string("<root/>", opts).expect("parse")
205 }
206
207 #[test]
208 fn new_sets_name_defaults() {
209 let c = Collector::new("MyCollector");
210 assert_eq!(c.get_name(), "MyCollector");
211 assert!(c.resource_directory.is_none());
212 assert!(c.resource_prefix.is_none());
213 assert!(!c.has_scanner);
214 }
215
216 #[test]
217 fn with_scanner_flips_flag() {
218 let c = Collector::new("X").with_scanner();
219 assert!(c.has_scanner);
220 }
221
222 #[test]
223 fn get_page_name_no_destination_is_bare_initial_xml() {
224 let doc = doc_with_dest(None);
225 assert_eq!(get_page_name(&doc, "A"), "A.xml");
226 }
227
228 #[test]
229 fn get_page_name_index_uses_just_initial() {
230 let doc = doc_with_dest(Some("/tmp/index.html"));
231 assert_eq!(get_page_name(&doc, "A"), "/tmp/A.html");
233 }
234
235 #[test]
236 fn get_page_name_non_index_appends_initial() {
237 let doc = doc_with_dest(Some("/tmp/doc.html"));
238 assert_eq!(get_page_name(&doc, "A"), "/tmp/doc.A.html");
239 }
240
241 #[test]
242 fn get_page_name_preserves_extension() {
243 let doc = doc_with_dest(Some("/tmp/foo.xml"));
244 assert_eq!(get_page_name(&doc, "B"), "/tmp/foo.B.xml");
245 }
246
247 #[test]
248 fn make_sub_collection_documents_empty_map_returns_empty() {
249 let mut doc = doc_with_dest(None);
250 let root = doc.get_document_element().expect("root");
251 let collections: HashMap<String, Vec<NodeData>> = HashMap::default();
252 let result = make_sub_collection_documents(&mut doc, &root, &collections);
253 assert!(result.is_empty());
254 }
255
256 #[test]
257 fn make_sub_collection_documents_populated_map_currently_returns_empty() {
258 let mut doc = doc_with_dest(None);
261 let root = doc.get_document_element().expect("root");
262 let mut collections: HashMap<String, Vec<NodeData>> = HashMap::default();
263 collections.insert("A".to_string(), vec![NodeData::Text("entry-a".to_string())]);
264 collections.insert("B".to_string(), vec![NodeData::Text("entry-b".to_string())]);
265 let result = make_sub_collection_documents(&mut doc, &root, &collections);
266 assert!(result.is_empty());
267 }
268}