Skip to main content

quarb_xml/
lib.rs

1//! XML adapter for Quarb.
2//!
3//! Maps a well-formed XML document onto the arbor model, parsing
4//! with `quick-xml`. Unlike the HTML adapter, parsing is strict:
5//! mismatched or unclosed tags, duplicate attributes, and unknown
6//! entity references are errors.
7//!
8//! - Elements are the nodes; text, CDATA, comments, processing
9//!   instructions, and the doctype are not navigated (text is
10//!   exposed as a projection instead).
11//! - A node's *name* is its tag exactly as written, including any
12//!   namespace prefix (`dc:title`). Prefixes are not resolved to
13//!   URIs. Since `:` is not a bare-name character in Quarb, a
14//!   prefixed name is navigated with a quoted segment
15//!   (`//'dc:title'`); `;;;local-name` and `;;;ns-prefix` metadata
16//!   allow prefix-agnostic filtering (`//*[;;;local-name =
17//!   "title"]`). The document root is unnamed; the document
18//!   element is its single child.
19//! - Nodes have no traits: XML has no universal structural
20//!   vocabulary, and leaf/container distinctions are already
21//!   available as core metadata (`:::is-leaf`, `:::n-children`).
22//! - Attributes are properties: `::pages`, `::href`. The default
23//!   projection (`::`) and `::text` are the element's text
24//!   content; `;;;tag`, `;;;id`, `;;;local-name`, `;;;ns-prefix`,
25//!   `;;;n-attrs`, and any `;;;attr` expose metadata.
26//! - An attribute holding an ID reference resolves: `::ref~>`
27//!   follows a bare IDREF value (`book="b1"`) or a fragment
28//!   (`href="#b1"`) to the element with that `id` or `xml:id`.
29//! - Input is UTF-8 text; an `encoding` declaration in the XML
30//!   prologue is ignored. Only the five predefined entities and
31//!   numeric character references are resolved.
32
33use quarb::{AstAdapter, NodeId, Value};
34use quick_xml::escape::resolve_predefined_entity;
35use quick_xml::events::{BytesStart, Event};
36use quick_xml::{Decoder, Reader};
37use std::collections::HashMap;
38
39struct Node {
40    /// The tag name as written; `None` for the document root.
41    tag: Option<String>,
42    attrs: Vec<(String, String)>,
43    /// The element's text content (all descendant text and CDATA,
44    /// concatenated in document order).
45    text: String,
46    parent: Option<NodeId>,
47    children: Vec<NodeId>,
48}
49
50impl Node {
51    fn attr(&self, name: &str) -> Option<&str> {
52        self.attrs
53            .iter()
54            .find(|(k, _)| k == name)
55            .map(|(_, v)| v.as_str())
56    }
57}
58
59/// An error building an [`XmlAdapter`] from document text.
60#[derive(Debug, thiserror::Error)]
61pub enum XmlError {
62    #[error(transparent)]
63    Syntax(#[from] quick_xml::Error),
64    #[error("unknown entity reference '&{0};'")]
65    UnknownEntity(String),
66    #[error("unclosed element '<{0}>'")]
67    Unclosed(String),
68    #[error("no root element")]
69    NoRoot,
70    #[error("content after the root element")]
71    TrailingContent,
72}
73
74/// A Quarb adapter over a parsed XML document.
75pub struct XmlAdapter {
76    nodes: Vec<Node>,
77    /// `id` / `xml:id` attribute value → the element carrying it.
78    ids: HashMap<String, NodeId>,
79    root: NodeId,
80}
81
82impl XmlAdapter {
83    /// Parse `xml` and build the adapter. The document must be
84    /// well-formed: exactly one root element, matched tags, unique
85    /// attributes, and only predefined or numeric entities.
86    pub fn parse(xml: &str) -> Result<Self, XmlError> {
87        let mut nodes = vec![Node {
88            tag: None,
89            attrs: Vec::new(),
90            text: String::new(),
91            parent: None,
92            children: Vec::new(),
93        }];
94        let mut ids = HashMap::new();
95        let root = NodeId(0);
96
97        let mut reader = Reader::from_str(xml);
98        let decoder = reader.decoder();
99        // Indices of the currently open elements; the synthetic
100        // root stays at the bottom.
101        let mut stack: Vec<usize> = vec![0];
102
103        loop {
104            match reader.read_event()? {
105                Event::Start(e) => {
106                    let idx = intern(&mut nodes, &mut ids, &stack, decoder, &e)?;
107                    stack.push(idx);
108                }
109                Event::Empty(e) => {
110                    intern(&mut nodes, &mut ids, &stack, decoder, &e)?;
111                }
112                Event::End(_) => {
113                    let closed = stack.pop().expect("end event matches an open element");
114                    let text = std::mem::take(&mut nodes[closed].text);
115                    let parent = *stack.last().expect("stack holds the root");
116                    nodes[parent].text.push_str(&text);
117                    nodes[closed].text = text;
118                }
119                Event::Text(e) => {
120                    let open = *stack.last().expect("stack holds the root");
121                    let text = e.xml_content().map_err(quick_xml::Error::from)?;
122                    nodes[open].text.push_str(&text);
123                }
124                Event::CData(e) => {
125                    let open = *stack.last().expect("stack holds the root");
126                    let text = e.decode().map_err(quick_xml::Error::from)?;
127                    nodes[open].text.push_str(&text);
128                }
129                Event::GeneralRef(e) => {
130                    let open = *stack.last().expect("stack holds the root");
131                    if let Some(ch) = e.resolve_char_ref()? {
132                        nodes[open].text.push(ch);
133                    } else {
134                        let name = e.decode().map_err(quick_xml::Error::from)?;
135                        let Some(resolved) = resolve_predefined_entity(&name) else {
136                            return Err(XmlError::UnknownEntity(name.into_owned()));
137                        };
138                        nodes[open].text.push_str(resolved);
139                    }
140                }
141                Event::Decl(_) | Event::PI(_) | Event::Comment(_) | Event::DocType(_) => {}
142                Event::Eof => break,
143            }
144        }
145
146        if stack.len() > 1 {
147            let open = stack.pop().expect("checked non-root");
148            let tag = nodes[open].tag.clone().expect("open elements are tagged");
149            return Err(XmlError::Unclosed(tag));
150        }
151        match nodes[0].children.len() {
152            0 => return Err(XmlError::NoRoot),
153            1 => {}
154            _ => return Err(XmlError::TrailingContent),
155        }
156        // The synthetic root's text is the document element's, not
157        // any stray whitespace around it.
158        nodes[0].text = nodes[nodes[0].children[0].0 as usize].text.clone();
159
160        Ok(XmlAdapter { nodes, ids, root })
161    }
162
163    /// A locator path to `node`, like `/library/book[2]/author`, for
164    /// rendering. A `[n]` index is added only to disambiguate
165    /// same-tag siblings.
166    pub fn locator(&self, node: NodeId) -> String {
167        let mut segments = Vec::new();
168        let mut cur = Some(node);
169        while let Some(id) = cur {
170            let n = &self.nodes[id.0 as usize];
171            if let Some(tag) = &n.tag {
172                segments.push(self.segment(id, tag));
173            }
174            cur = n.parent;
175        }
176        segments.reverse();
177        format!("/{}", segments.join("/"))
178    }
179
180    fn segment(&self, node: NodeId, tag: &str) -> String {
181        let Some(parent) = self.nodes[node.0 as usize].parent else {
182            return tag.to_string();
183        };
184        let siblings = &self.nodes[parent.0 as usize].children;
185        let same_tag: Vec<NodeId> = siblings
186            .iter()
187            .copied()
188            .filter(|&s| self.nodes[s.0 as usize].tag.as_deref() == Some(tag))
189            .collect();
190        if same_tag.len() > 1 {
191            let n = same_tag.iter().position(|&s| s == node).unwrap() + 1;
192            format!("{tag}[{n}]")
193        } else {
194            tag.to_string()
195        }
196    }
197}
198
199/// Intern the element behind a `Start`/`Empty` event as a child of
200/// the innermost open element, returning its arena index.
201fn intern(
202    nodes: &mut Vec<Node>,
203    ids: &mut HashMap<String, NodeId>,
204    stack: &[usize],
205    decoder: Decoder,
206    e: &BytesStart,
207) -> Result<usize, XmlError> {
208    let idx = nodes.len();
209    let this = NodeId(idx as u64);
210    let tag = decoder
211        .decode(e.name().as_ref())
212        .map_err(quick_xml::Error::from)?
213        .into_owned();
214    let mut attrs = Vec::new();
215    for attr in e.attributes() {
216        let attr = attr.map_err(quick_xml::Error::from)?;
217        let key = decoder
218            .decode(attr.key.as_ref())
219            .map_err(quick_xml::Error::from)?
220            .into_owned();
221        let value = attr.decode_and_unescape_value(decoder)?.into_owned();
222        if key == "id" || key == "xml:id" {
223            ids.entry(value.clone()).or_insert(this);
224        }
225        attrs.push((key, value));
226    }
227    let parent = *stack.last().expect("stack holds the root");
228    nodes.push(Node {
229        tag: Some(tag),
230        attrs,
231        text: String::new(),
232        parent: Some(NodeId(parent as u64)),
233        children: Vec::new(),
234    });
235    nodes[parent].children.push(this);
236    Ok(idx)
237}
238
239/// The name before / after the first `:` of a qualified name.
240fn split_qualified(tag: &str) -> (Option<&str>, &str) {
241    match tag.split_once(':') {
242        Some((prefix, local)) => (Some(prefix), local),
243        None => (None, tag),
244    }
245}
246
247impl AstAdapter for XmlAdapter {
248    fn root(&self) -> NodeId {
249        self.root
250    }
251
252    fn children(&self, node: NodeId) -> Vec<NodeId> {
253        self.nodes[node.0 as usize].children.clone()
254    }
255
256    fn name(&self, node: NodeId) -> Option<String> {
257        self.nodes[node.0 as usize].tag.clone()
258    }
259
260    fn parent(&self, node: NodeId) -> Option<NodeId> {
261        self.nodes[node.0 as usize].parent
262    }
263
264    /// `::text` is the element's text content; any other name is an
265    /// attribute value.
266    /// Attributes, by name (`::pages`). Element text is the bare
267    /// projection (`::`) — no attribute name is shadowed.
268    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
269        self.nodes[node.0 as usize]
270            .attr(name)
271            .map(|v| Value::Str(v.to_string()))
272    }
273
274    /// The default projection of an element is its text content.
275    fn default_value(&self, node: NodeId) -> Option<Value> {
276        Some(Value::Str(self.nodes[node.0 as usize].text.clone()))
277    }
278
279    /// `;;;tag`, `;;;local-name`, `;;;ns-prefix`, and `;;;n-attrs`
280    /// — facts about the element, never its data: attributes are
281    /// properties (`::pages`), text is the bare projection.
282    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
283        let n = &self.nodes[node.0 as usize];
284        match key {
285            "tag" => n.tag.clone().map(Value::Str),
286            "local-name" => n
287                .tag
288                .as_deref()
289                .map(|t| Value::Str(split_qualified(t).1.to_string())),
290            "ns-prefix" => n
291                .tag
292                .as_deref()
293                .and_then(|t| split_qualified(t).0)
294                .map(|p| Value::Str(p.to_string())),
295            "n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
296            _ => None,
297        }
298    }
299
300    /// Follow an attribute that is an ID reference — a bare IDREF
301    /// value (`book="b1"`) or a fragment (`href="#b1"`) — to the
302    /// element carrying that `id` or `xml:id`.
303    fn resolve(&self, node: NodeId, property: &str, _hint: Option<&str>) -> Option<NodeId> {
304        let value = self.nodes[node.0 as usize].attr(property)?;
305        let target = value.strip_prefix('#').unwrap_or(value);
306        self.ids.get(target).copied()
307    }
308}