latexml_core/sxml/
fragment_reader.rs1use libxml::{reader::TextReader, readonly::RoNode, tree::Document};
17
18use crate::common::error::{Error, ErrorCategory, ErrorTarget, Result};
19
20pub struct FragmentReader {
23 reader: TextReader,
24 entered: bool,
27}
28
29impl FragmentReader {
30 pub fn open(path: &std::path::Path) -> Result<Self> {
34 let reader = TextReader::from_file(&path.to_string_lossy(), 0)
35 .map_err(|()| reader_error(format!("cannot open {}", path.display())))?;
36 Ok(FragmentReader { reader, entered: false })
37 }
38
39 pub fn next_fragment(&mut self) -> Result<Option<Document>> {
44 loop {
45 let advanced = if self.entered && self.at_fragment() {
46 self.reader.read_next()
49 } else {
50 self.reader.read()
51 }
52 .map_err(|()| reader_error(String::from("parse error while streaming")))?;
53 if !advanced {
54 return Ok(None);
55 }
56 if !self.entered {
57 if self.reader.is_element() && self.reader.depth() == 0 {
59 self.entered = true;
60 }
61 continue;
62 }
63 if self.at_fragment() {
64 let doc = self
65 .reader
66 .expand_to_document()
67 .ok_or_else(|| reader_error(String::from("expand_to_document failed on a fragment")))?;
68 return Ok(Some(doc));
69 }
70 }
73 }
74
75 fn at_fragment(&self) -> bool { self.reader.is_element() && self.reader.depth() == 1 }
77
78 pub fn peek(&self) -> Option<RoNode> { self.reader.expand() }
82}
83
84fn reader_error(details: String) -> Error {
85 Error {
86 target: ErrorTarget::Internal,
87 category: ErrorCategory::Libxml,
88 message: format!("fragment-reader: {details}"),
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn write_fixture(name: &str, content: &str) -> std::path::PathBuf {
97 let path = std::env::temp_dir().join(format!("lxsxml-reader-{}-{name}", std::process::id()));
98 std::fs::write(&path, content).unwrap();
99 path
100 }
101
102 #[test]
103 fn iterates_fragments_one_at_a_time() {
104 let path = write_fixture(
105 "basic.xml",
106 "<_lxfragment xmlns:ltx=\"http://dlmf.nist.gov/LaTeXML\">\
107 <ltx:para xml:id=\"p1\"><ltx:p>first Gr\u{00fc}\u{00df}e</ltx:p></ltx:para>\
108 <!-- a comment between fragments -->\
109 <ltx:para xml:id=\"p2\"><ltx:p>second \u{6570}\u{5b66}</ltx:p></ltx:para>\
110 <ltx:pagination role=\"newpage\"/>\
111 </_lxfragment>",
112 );
113 let mut reader = FragmentReader::open(&path).expect("open");
114
115 let mut seen = Vec::new();
116 while let Some(doc) = reader.next_fragment().expect("stream") {
117 let root = doc.get_root_element().expect("fragment root");
118 assert_eq!(
121 root.get_namespace().map(|ns| ns.get_href()),
122 Some(String::from("http://dlmf.nist.gov/LaTeXML")),
123 "fragment root keeps the wrapper-declared namespace"
124 );
125 seen.push((root.get_name(), root.get_content()));
126 }
127 let _ = std::fs::remove_file(&path);
128
129 assert_eq!(seen.len(), 3, "three fragments, comment skipped: {seen:?}");
130 assert_eq!(seen[0].0, "para");
131 assert!(seen[0].1.contains("first Gr\u{00fc}\u{00df}e"));
132 assert_eq!(seen[1].0, "para");
133 assert!(seen[1].1.contains("second \u{6570}\u{5b66}"));
134 assert_eq!(seen[2].0, "pagination", "empty-element fragment survives");
135 }
136
137 #[test]
138 fn fragment_documents_are_independent_and_mutable() {
139 let path = write_fixture(
140 "mutable.xml",
141 "<root><a n=\"1\"><child/></a><b n=\"2\"/></root>",
142 );
143 let mut reader = FragmentReader::open(&path).expect("open");
144
145 let doc_a = reader.next_fragment().expect("stream").expect("fragment a");
146 let mut root_a = doc_a.get_root_element().unwrap();
147 root_a.set_attribute("mutated", "yes").expect("mutable");
149
150 let doc_b = reader.next_fragment().expect("stream").expect("fragment b");
151 let root_b = doc_b.get_root_element().unwrap();
152 assert_eq!(root_b.get_name(), "b");
153 assert_eq!(root_b.get_attribute("n").as_deref(), Some("2"));
154 assert_eq!(root_a.get_attribute("mutated").as_deref(), Some("yes"));
155
156 assert!(
157 reader.next_fragment().expect("stream").is_none(),
158 "exhausted"
159 );
160 let _ = std::fs::remove_file(&path);
161 }
162
163 #[test]
164 fn open_missing_file_is_an_error() {
165 let missing = std::env::temp_dir().join("lxsxml-definitely-not-here.xml");
166 assert!(FragmentReader::open(&missing).is_err());
167 }
168
169 #[test]
170 fn segment_store_round_trip_streams_back() {
171 use crate::sxml::{SegmentMeta, SegmentStore};
174 let tmp = std::env::temp_dir().join(format!("lxsxml-integ-{}", std::process::id()));
175 std::fs::create_dir_all(&tmp).unwrap();
176 let mut store = SegmentStore::create(&tmp).expect("store");
177 let meta = SegmentMeta {
178 depth: 1,
179 noindent: false,
180 section_id: None,
181 parent: None,
182 ancestors: vec![],
183 font: None,
184 namespaces: vec![(
185 String::from("ltx"),
186 String::from("http://dlmf.nist.gov/LaTeXML"),
187 )],
188 };
189 let id = store
190 .write_segment(
191 "<ltx:section xml:id=\"S1\"><ltx:title>One</ltx:title></ltx:section>\
192 <ltx:section xml:id=\"S2\"><ltx:title>Two</ltx:title></ltx:section>",
193 meta,
194 )
195 .expect("write");
196
197 let wrapped_path = tmp.join("wrapped.xml");
199 std::fs::write(&wrapped_path, store.wrapped_segment(id).expect("wrap")).unwrap();
200 let mut reader = FragmentReader::open(&wrapped_path).expect("open segment");
201 let mut titles = Vec::new();
202 while let Some(doc) = reader.next_fragment().expect("stream") {
203 let root = doc.get_root_element().unwrap();
204 assert_eq!(root.get_name(), "section");
205 titles.push(root.get_content());
206 }
207 assert_eq!(titles, vec![String::from("One"), String::from("Two")]);
208 drop(store);
209 let _ = std::fs::remove_dir_all(&tmp);
210 }
211}