1use std::{
18 fs,
19 io::{self, Write},
20 path::Path,
21};
22
23use libxml::tree::{Node, SaveOptions};
24
25use crate::{
26 document::{PostDocument, get_xml_id},
27 processor::{PostError, ProcessResult, Processor},
28};
29
30pub fn write_output(content: &str, dest: Option<&str>) -> io::Result<()> {
37 match dest {
38 Some(path) => {
39 ensure_parent_dir(path)?;
40 fs::write(path, content)?;
41 Info!(
42 "writer",
43 "wrote",
44 "Wrote '{}' ({} bytes)",
45 path,
46 content.len()
47 );
48 Ok(())
49 },
50 None => io::stdout().write_all(content.as_bytes()),
51 }
52}
53
54pub fn write_output_segments(segments: &[&str], dest: Option<&str>) -> io::Result<()> {
62 match dest {
63 Some(path) => {
64 ensure_parent_dir(path)?;
65 let mut writer = io::BufWriter::new(fs::File::create(path)?);
66 let mut total = 0usize;
67 for seg in segments {
68 writer.write_all(seg.as_bytes())?;
69 total += seg.len();
70 }
71 writer.flush()?;
72 Info!("writer", "wrote", "Wrote '{}' ({} bytes)", path, total);
73 Ok(())
74 },
75 None => {
76 let stdout = io::stdout();
77 let mut handle = stdout.lock();
78 for seg in segments {
79 handle.write_all(seg.as_bytes())?;
80 }
81 Ok(())
82 },
83 }
84}
85
86pub fn ensure_parent_dir(path: &str) -> io::Result<()> {
90 if let Some(parent) = Path::new(path).parent() {
91 if !parent.as_os_str().is_empty() {
92 fs::create_dir_all(parent)?;
93 }
94 }
95 Ok(())
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum OutputFormat {
101 Xml,
102 Html,
103}
104
105pub struct Writer {
109 name: String,
110 format: OutputFormat,
111 omit_doctype: bool,
112 is_html: bool,
113}
114
115impl Writer {
116 pub fn new(format: Option<OutputFormat>, omit_doctype: bool, is_html: bool) -> Self {
117 Writer {
118 name: "Writer".to_string(),
119 format: format.unwrap_or(OutputFormat::Xml),
120 omit_doctype,
121 is_html,
122 }
123 }
124}
125
126impl Processor for Writer {
127 fn get_name(&self) -> &str { &self.name }
128
129 fn to_process(&self, doc: &PostDocument) -> Vec<Node> {
130 match doc.get_document_element() {
131 Some(el) => vec![el],
132 None => vec![],
133 }
134 }
135
136 fn process(&mut self, mut doc: PostDocument, nodes: Vec<Node>) -> ProcessResult {
137 let mut root = match nodes.into_iter().next() {
138 Some(r) => r,
139 None => return Ok(vec![doc]),
140 };
141
142 if self.omit_doctype {
147 doc.get_document_mut().remove_internal_subset();
148 }
149
150 if let Some(id) = get_xml_id(&root) {
156 if id == "TEMPORARY_DOCUMENT_ID" {
157 let _ = root.remove_attribute_ns("id", latexml_core::common::xml::XML_NS);
158 }
159 }
160
161 let serialized = if self.is_html {
163 doc.get_document().to_string_with_options(SaveOptions {
164 as_html: true,
165 format: true,
166 ..SaveOptions::default()
167 })
168 } else {
169 doc.get_document().to_string_with_options(SaveOptions {
170 format: true,
171 ..SaveOptions::default()
172 })
173 };
174
175 if let Some(destination) = doc.get_destination() {
176 if let Some(destdir) = doc.get_destination_directory() {
178 fs::create_dir_all(destdir).map_err(|e| {
179 PostError::Io(io::Error::new(
180 e.kind(),
181 format!("Couldn't create directory '{}': {}", destdir, e),
182 ))
183 })?;
184 }
185 fs::write(destination, &serialized).map_err(|e| {
186 PostError::Io(io::Error::new(
187 e.kind(),
188 format!("Couldn't write '{}': {}", destination, e),
189 ))
190 })?;
191 Info!(
192 "writer",
193 "wrote",
194 "Wrote '{}' ({})",
195 destination,
196 serialized.len()
197 );
198 } else {
199 print!("{}", serialized);
200 }
201
202 Ok(vec![doc])
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn output_format_variants() {
212 let _ = OutputFormat::Xml;
213 let _ = OutputFormat::Html;
214 }
215
216 #[test]
217 fn output_format_partial_eq() {
218 assert_eq!(OutputFormat::Xml, OutputFormat::Xml);
219 assert_ne!(OutputFormat::Xml, OutputFormat::Html);
220 assert_eq!(OutputFormat::Html, OutputFormat::Html);
221 }
222
223 #[test]
224 fn output_format_copy_clone() {
225 let a = OutputFormat::Xml;
227 let b = a;
228 let _ = a; assert_eq!(a, b);
230 }
231
232 #[test]
233 fn writer_new_default_format_xml() {
234 let w = Writer::new(None, false, false);
235 assert_eq!(w.get_name(), "Writer");
236 assert_eq!(w.format, OutputFormat::Xml);
237 assert!(!w.omit_doctype);
238 assert!(!w.is_html);
239 }
240
241 #[test]
242 fn writer_new_explicit_format() {
243 let w = Writer::new(Some(OutputFormat::Html), true, true);
244 assert_eq!(w.format, OutputFormat::Html);
245 assert!(w.omit_doctype);
246 assert!(w.is_html);
247 }
248
249 #[test]
250 fn writer_get_name_is_writer() {
251 let w = Writer::new(None, false, false);
252 assert_eq!(w.get_name(), "Writer");
253 }
254
255 #[test]
256 fn writer_omit_doctype_strips_doctype_preamble() {
260 use crate::document::{PostDocument, PostDocumentOptions};
261
262 let xml = r#"<?xml version="1.0"?>
263<!DOCTYPE root SYSTEM "example.dtd">
264<root><child>hi</child></root>"#;
265 let doc = PostDocument::new_from_string(xml, PostDocumentOptions::default())
266 .expect("parse test fixture");
267
268 let mut writer = Writer::new(
270 None, true, false,
271 );
272 let to_process = writer.to_process(&doc);
273 let result = writer.process(doc, to_process).expect("process");
274 let after = result
275 .into_iter()
276 .next()
277 .expect("at least one doc")
278 .get_document()
279 .to_string();
280 assert!(
281 !after.contains("<!DOCTYPE"),
282 "expected DOCTYPE stripped, got: {after}"
283 );
284 assert!(after.contains("<root>"));
285 }
286
287 #[test]
288 fn writer_default_preserves_doctype() {
291 use crate::document::{PostDocument, PostDocumentOptions};
292
293 let xml = r#"<?xml version="1.0"?>
294<!DOCTYPE root SYSTEM "example.dtd">
295<root><child>hi</child></root>"#;
296 let doc = PostDocument::new_from_string(xml, PostDocumentOptions::default())
297 .expect("parse test fixture");
298
299 let mut writer = Writer::new(
300 None, false, false,
301 );
302 let to_process = writer.to_process(&doc);
303 let result = writer.process(doc, to_process).expect("process");
304 let after = result
305 .into_iter()
306 .next()
307 .expect("at least one doc")
308 .get_document()
309 .to_string();
310 assert!(
311 after.contains("<!DOCTYPE"),
312 "expected DOCTYPE preserved, got: {after}"
313 );
314 }
315}