1use std::{fmt::Write as _, fs, path::Path};
22
23use rustc_hash::FxHashMap;
24
25use super::SegmentId;
26use crate::common::error::{Error, ErrorCategory, ErrorTarget, Result};
27
28#[derive(Debug, Default)]
31pub struct FragmentIndex {
32 ids: FxHashMap<String, SegmentId>,
33 labels: FxHashMap<String, String>,
34 rdfa_prefixes: FxHashMap<String, String>,
35}
36
37impl FragmentIndex {
38 pub fn record_id(&mut self, id: &str, segment: SegmentId) {
40 self.ids.insert(id.to_string(), segment);
41 }
42
43 pub fn record_label(&mut self, label: &str, id: &str) {
45 self.labels.insert(label.to_string(), id.to_string());
46 }
47
48 pub fn record_rdfa_prefix(&mut self, prefix: &str, uri: &str) {
50 self
51 .rdfa_prefixes
52 .insert(prefix.to_string(), uri.to_string());
53 }
54
55 pub fn id_segment(&self, id: &str) -> Option<SegmentId> { self.ids.get(id).copied() }
57
58 pub fn contains_id(&self, id: &str) -> bool { self.ids.contains_key(id) }
61
62 pub fn label_id(&self, label: &str) -> Option<&str> { self.labels.get(label).map(String::as_str) }
64
65 pub fn labels(&self) -> impl Iterator<Item = (&str, &str)> {
69 self.labels.iter().map(|(l, i)| (l.as_str(), i.as_str()))
70 }
71
72 pub fn sizes(&self) -> (usize, usize, usize) {
74 (self.ids.len(), self.labels.len(), self.rdfa_prefixes.len())
75 }
76
77 pub fn rdfa_prefixes(&self) -> impl Iterator<Item = (&str, &str)> {
79 self
80 .rdfa_prefixes
81 .iter()
82 .map(|(p, u)| (p.as_str(), u.as_str()))
83 }
84
85 pub fn id_count(&self) -> usize { self.ids.len() }
87
88 pub fn save(&self, path: &Path) -> Result<()> {
92 let mut out = String::new();
93 for (id, seg) in &self.ids {
94 let _ = writeln!(out, "id\t{}\t{}", escape(id), seg.0);
95 }
96 for (label, id) in &self.labels {
97 let _ = writeln!(out, "label\t{}\t{}", escape(label), escape(id));
98 }
99 for (prefix, uri) in &self.rdfa_prefixes {
100 let _ = writeln!(out, "rdfa\t{}\t{}", escape(prefix), escape(uri));
101 }
102 fs::write(path, out).map_err(|e| index_error(format!("write {}: {e}", path.display())))
103 }
104
105 pub fn load(path: &Path) -> Result<Self> {
109 let text =
110 fs::read_to_string(path).map_err(|e| index_error(format!("read {}: {e}", path.display())))?;
111 let mut index = FragmentIndex::default();
112 for (n, line) in text.lines().enumerate() {
113 if line.is_empty() {
114 continue;
115 }
116 let mut parts = line.splitn(3, '\t');
117 let (kind, key, value) = match (parts.next(), parts.next(), parts.next()) {
118 (Some(k), Some(key), Some(v)) => (k, unescape(key), unescape(v)),
119 _ => {
120 return Err(index_error(format!(
121 "malformed line {} in {}",
122 n + 1,
123 path.display()
124 )));
125 },
126 };
127 match kind {
128 "id" => {
129 let seg: u32 = value
130 .parse()
131 .map_err(|_| index_error(format!("bad segment number {value:?} at line {}", n + 1)))?;
132 index.ids.insert(key, SegmentId(seg));
133 },
134 "label" => {
135 index.labels.insert(key, value);
136 },
137 "rdfa" => {
138 index.rdfa_prefixes.insert(key, value);
139 },
140 other => {
141 return Err(index_error(format!(
142 "unknown record kind {other:?} at line {} in {}",
143 n + 1,
144 path.display()
145 )));
146 },
147 }
148 }
149 Ok(index)
150 }
151}
152
153fn escape(s: &str) -> String {
154 s.replace('%', "%25")
156 .replace('\t', "%09")
157 .replace('\n', "%0A")
158}
159
160fn unescape(s: &str) -> String {
161 s.replace("%0A", "\n")
162 .replace("%09", "\t")
163 .replace("%25", "%")
164}
165
166fn index_error(details: String) -> Error {
167 Error {
168 target: ErrorTarget::Internal,
169 category: ErrorCategory::Unexpected,
170 message: format!("fragment-index: {details}"),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn records_and_queries() {
180 let mut index = FragmentIndex::default();
181 index.record_id("S1.p3", SegmentId(0));
182 index.record_id("S2.p1", SegmentId(4));
183 index.record_label("LABEL:intro", "S1.p3");
184 index.record_rdfa_prefix("dct", "http://purl.org/dc/terms/");
185
186 assert_eq!(index.id_segment("S1.p3"), Some(SegmentId(0)));
187 assert_eq!(index.id_segment("S2.p1"), Some(SegmentId(4)));
188 assert_eq!(index.id_segment("missing"), None);
189 assert!(index.contains_id("S1.p3"));
190 assert!(!index.contains_id("LABEL:intro"));
191 assert_eq!(index.label_id("LABEL:intro"), Some("S1.p3"));
192 assert_eq!(index.id_count(), 2);
193 let prefixes: Vec<_> = index.rdfa_prefixes().collect();
194 assert_eq!(prefixes, vec![("dct", "http://purl.org/dc/terms/")]);
195 }
196
197 #[test]
198 fn save_load_round_trip_with_awkward_keys() {
199 let mut index = FragmentIndex::default();
200 index.record_id("id\twith\ttabs", SegmentId(1));
202 index.record_id("id\nnewline", SegmentId(2));
203 index.record_label("LABEL:100%\u{6570}", "S1.E5");
204 index.record_rdfa_prefix("foaf", "http://xmlns.com/foaf/0.1/");
205
206 let tmp = std::env::temp_dir().join(format!("lxsxml-index-{}.tsv", std::process::id()));
207 index.save(&tmp).expect("save");
208 let loaded = FragmentIndex::load(&tmp).expect("load");
209 let _ = fs::remove_file(&tmp);
210
211 assert_eq!(loaded.id_segment("id\twith\ttabs"), Some(SegmentId(1)));
212 assert_eq!(loaded.id_segment("id\nnewline"), Some(SegmentId(2)));
213 assert_eq!(loaded.label_id("LABEL:100%\u{6570}"), Some("S1.E5"));
214 assert_eq!(loaded.rdfa_prefixes().collect::<Vec<_>>(), vec![(
215 "foaf",
216 "http://xmlns.com/foaf/0.1/"
217 )]);
218 }
219
220 #[test]
221 fn load_rejects_unknown_kinds_instead_of_skipping() {
222 let tmp = std::env::temp_dir().join(format!("lxsxml-badidx-{}.tsv", std::process::id()));
223 fs::write(&tmp, "mystery\tkey\tvalue\n").unwrap();
224 let result = FragmentIndex::load(&tmp);
225 let _ = fs::remove_file(&tmp);
226 assert!(result.is_err(), "unknown record kinds must fail loudly");
227 }
228
229 #[test]
239 fn xml_id_readable_in_both_attribute_forms() {
240 use libxml::{parser::Parser, tree::Node};
241
242 let parsed = Parser::default()
244 .parse_string(r#"<r xmlns="urn:x"><s xml:id="parsed.id"/></r>"#)
245 .expect("parse");
246 let s_node = parsed
247 .get_root_element()
248 .and_then(|r| r.get_first_element_child())
249 .expect("child");
250 assert_eq!(
251 crate::document::Document::node_xml_id_any_form(&s_node).as_deref(),
252 Some("parsed.id"),
253 "namespaced form must read"
254 );
255
256 let built = Parser::default().parse_string("<r/>").expect("parse shell");
258 let mut root = built.get_root_element().expect("root");
259 let mut child = Node::new("s", None, &built).expect("node");
260 child.set_attribute("xml:id", "built.id").expect("attr");
261 root.add_child(&mut child).expect("attach");
262 assert_eq!(
263 child.get_attribute_ns("id", "http://www.w3.org/XML/1998/namespace"),
264 Some("built.id".to_string()),
265 "set_attribute must namespace xml:id (probed libxml 0.3.21 behavior)"
266 );
267 assert_eq!(
268 crate::document::Document::node_xml_id_any_form(&child).as_deref(),
269 Some("built.id"),
270 "constructed form must read through the helper too"
271 );
272 }
273}