Skip to main content

sup_xml_core/
stream_parser.rs

1//! Streaming pull parser that yields arena-allocated subtrees.
2//!
3//! The arena counterpart of [`crate::stream_parser`].  Each emitted subtree
4//! is its own [`Document`] with its own [`bumpalo::Bump`] arena — drop the
5//! `Document` and that subtree's memory is freed.  Memory is bounded by the
6//! largest single emitted subtree, regardless of overall document size.
7//!
8//! # API surface
9//!
10//! * [`emit_at_depth(d)`](StreamParser::emit_at_depth) — emit elements
11//!   at a fixed depth from the root.
12//! * [`emit_at_path(&[…])`](StreamParser::emit_at_path) — emit elements
13//!   whose root-anchored ancestor chain matches `path` exactly.
14//! * [`emit_when(predicate)`](StreamParser::emit_when) — emit elements
15//!   whose ancestor chain satisfies an arbitrary closure.  Useful for
16//!   "any `<item>` regardless of nesting" / "anything two levels under
17//!   `<rss>`" patterns the fixed-depth modes can't express.
18//!
19//! All three are *ancestor-bounded* modes: the parser inspects the
20//! freshly-opened element's name + ancestor chain to decide whether
21//! to materialize its subtree.  Elements above the emit boundary
22//! cost just a depth counter + name on a `Vec<String>` ancestor
23//! stack — independent of total document size.
24//!
25//! Namespace resolution runs per emission: each emitted `Document`'s
26//! nodes carry `namespace` set to the in-scope binding at the time
27//! the element was opened, inherited correctly through ancestor
28//! `xmlns:*` declarations.
29//!
30//! # Example
31//!
32//! ```
33//! use sup_xml_core::StreamParser;
34//!
35//! let xml = r#"<rss><channel>
36//!   <item><title>a</title></item>
37//!   <item><title>b</title></item>
38//! </channel></rss>"#;
39//!
40//! let mut sp = StreamParser::from_str(xml)
41//!     .emit_at_path(&["rss", "channel", "item"]);
42//!
43//! let mut titles = Vec::new();
44//! while let Some(item) = sp.next().unwrap() {
45//!     let t = item.root().find_child("title").unwrap();
46//!     titles.push(t.text_content().unwrap().to_owned());
47//!     // `item` (and its arena) drops at the end of this iteration.
48//! }
49//! assert_eq!(titles, vec!["a", "b"]);
50//! ```
51
52use rustc_hash::{FxHashMap, FxHashSet};
53
54use crate::error::Result;
55use crate::ns_helpers::{
56    ns_err, validate_qname, validate_xmlns_decl, XML_NS_URI, XMLNS_NS_URI,
57};
58use crate::options::ParseOptions;
59use crate::reader::{Attr, EventInto, XmlReader};
60use sup_xml_tree::dom::{Document, DocumentBuilder, Namespace, Node};
61
62// ── public type ─────────────────────────────────────────────────────────────
63
64/// Pull-based streaming parser that yields arena-allocated subtrees.
65pub struct StreamParser<'src> {
66    reader:   XmlReader<'src>,
67    attr_buf: Vec<Attr<'src>>,
68    emit:     EmitMode,
69    state:    State,
70}
71
72/// Predicate evaluated against the ancestor-name chain of the
73/// just-opened element.  Receives a slice whose last entry is the
74/// just-opened element's QName and earlier entries are its
75/// ancestors in order from the root.  Return `true` to emit that
76/// element's subtree.
77pub type EmitPredicate = Box<dyn Fn(&[String]) -> bool + Send + Sync + 'static>;
78
79/// Selection mode.  Three flavours, ordered by specificity.
80enum EmitMode {
81    /// Default — emit nothing.  Caller must specify a strategy.
82    None,
83    /// Emit elements whose post-pop ancestor depth equals this value.
84    /// `depth = 1` ⇒ direct children of the root.
85    Depth(u32),
86    /// Emit elements whose root-anchored ancestor chain matches `path`.
87    Path(Vec<String>),
88    /// Emit elements whose ancestor chain satisfies an arbitrary
89    /// predicate.  Less constrained than [`EmitMode::Path`] /
90    /// [`EmitMode::Depth`] — useful for "any `<book>` regardless of
91    /// where it appears" or "anything whose 4th ancestor is `<rss>`"
92    /// patterns.  Same memory profile as the fixed-depth modes:
93    /// elements above the emit boundary cost only the ancestor stack.
94    When(EmitPredicate),
95}
96
97struct State {
98    /// SAX depth.  0 before the root opens, 1 inside the root, etc.
99    sax_depth:  u32,
100    /// Names of currently-open ancestors (root at index 0).
101    name_stack: Vec<String>,
102    /// Persistent namespace scope tracked across every element, whether or
103    /// not it's being emitted.  Owned `(prefix, href)` pairs; layered by
104    /// `ns_frames`.  Built-in `xml`/`xmlns` bindings occupy the first two
105    /// entries and never get popped.
106    ns_bindings: Vec<(Option<String>, String)>,
107    /// Frame markers — one entry per open SAX element.  Indices into
108    /// `ns_bindings` recording where each element's bindings start.
109    ns_frames:   Vec<usize>,
110    /// In-progress emission, if we're inside a to-be-emitted subtree.
111    current:    Option<Emission>,
112}
113
114impl Default for State {
115    fn default() -> Self {
116        Self {
117            sax_depth:  0,
118            name_stack: Vec::new(),
119            ns_bindings: vec![
120                (Some("xml".to_string()),   XML_NS_URI.to_string()),
121                (Some("xmlns".to_string()), XMLNS_NS_URI.to_string()),
122            ],
123            ns_frames:  Vec::new(),
124            current:    None,
125        }
126    }
127}
128
129/// State for the subtree currently being assembled into its own arena.
130struct Emission {
131    builder:     DocumentBuilder,
132    /// SAX depth at which this emission started.  Ship when we close it.
133    start_depth: u32,
134    /// Stack of in-progress descendants (root of THIS subtree at index 0).
135    /// Type-erased — see parser for the same trick.
136    stack:       Vec<*const ()>,
137    /// Cache mapping `(prefix, href)` to an arena-allocated [`Namespace`]
138    /// pointer in this emission's `builder.bump`.  First lookup allocates;
139    /// repeated uses (within this subtree) share the same allocation.
140    ns_cache:    FxHashMap<(Option<String>, String), *const ()>,
141}
142
143// SAFETY note for raw pointers in `Emission::stack`: each pointer was obtained
144// from `&Node` allocated in `self.builder.bump`, which lives as long as the
145// `Emission` does.  Dereferencing while the `Emission` is alive is sound.
146
147#[inline] fn erase(node: &Node<'_>) -> *const () {
148    node as *const Node<'_> as *const ()
149}
150
151/// # Safety
152///
153/// `p` must have been produced by `erase` from a live `&Node`, and the
154/// caller-chosen lifetime `'a` must not outlive that node's arena.
155#[inline] unsafe fn unerase<'a>(p: *const ()) -> &'a Node<'a> {
156    unsafe { &*(p as *const Node<'a>) }
157}
158
159// ── public API ──────────────────────────────────────────────────────────────
160
161impl<'src> StreamParser<'src> {
162    pub fn from_str(input: &'src str) -> Self {
163        Self {
164            reader:   XmlReader::from_str(input),
165            attr_buf: Vec::new(),
166            emit:     EmitMode::None,
167            state:    State::default(),
168        }
169    }
170
171    pub fn from_bytes(input: &'src [u8]) -> Result<Self> {
172        Ok(Self {
173            reader:   XmlReader::from_bytes(input)?,
174            attr_buf: Vec::new(),
175            emit:     EmitMode::None,
176            state:    State::default(),
177        })
178    }
179
180    /// Replace [`ParseOptions`] on the underlying reader.  Takes a reference
181    /// (the reader internally clones once — amortized over the whole stream).
182    pub fn with_options(mut self, opts: &ParseOptions) -> Self {
183        self.reader = self.reader.with_options(opts.clone());
184        self
185    }
186
187    /// Emit elements at the given depth.  `depth = 1` matches direct children
188    /// of the root.  Memory-bounded.
189    pub fn emit_at_depth(mut self, depth: u32) -> Self {
190        self.emit = EmitMode::Depth(depth);
191        self
192    }
193
194    /// Emit elements whose ancestor chain (from root) matches `path` exactly.
195    /// `path[0]` is the root's name, `path[1]` its child, …, `path[N-1]` the
196    /// element to emit.  Memory-bounded.
197    pub fn emit_at_path(mut self, path: &[&str]) -> Self {
198        self.emit = EmitMode::Path(path.iter().map(|s| (*s).to_owned()).collect());
199        self
200    }
201
202    /// Emit elements whose ancestor chain satisfies `predicate`.  The
203    /// predicate receives a slice of QNames from root to the
204    /// just-opened element (inclusive at the end); return `true` to
205    /// emit that element's subtree.
206    ///
207    /// More flexible than [`emit_at_path`](Self::emit_at_path) /
208    /// [`emit_at_depth`](Self::emit_at_depth): handy when the
209    /// emission criterion is "any `<item>` regardless of where it
210    /// appears in the tree" or "any element under `<rss>` that is
211    /// itself named `entry`".
212    ///
213    /// Memory profile matches the fixed-depth modes — elements
214    /// above the matching boundary cost only the ancestor stack.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// use sup_xml_core::StreamParser;
220    /// let xml = r#"<root>
221    ///   <section><item>a</item></section>
222    ///   <other><item>b</item></other>
223    /// </root>"#;
224    /// let mut sp = StreamParser::from_str(xml)
225    ///     .emit_when(|chain| chain.last().map(String::as_str) == Some("item"));
226    /// while sp.next().unwrap().is_some() {}
227    /// ```
228    pub fn emit_when<F>(mut self, predicate: F) -> Self
229    where
230        F: Fn(&[String]) -> bool + Send + Sync + 'static,
231    {
232        self.emit = EmitMode::When(Box::new(predicate));
233        self
234    }
235
236    /// Pull the next emitted subtree.  Returns `Ok(Some(doc))` for each match,
237    /// `Ok(None)` on EOF, `Err(_)` on parse error.
238    #[allow(clippy::should_implement_trait)]
239    pub fn next(&mut self) -> Result<Option<Document>> {
240        loop {
241            let event = self.reader.next_into(&mut self.attr_buf)?;
242            match event {
243                EventInto::StartElement { name } => {
244                    let name = name.into_owned();
245                    self.state.sax_depth += 1;
246                    self.state.name_stack.push(name.clone());
247                    validate_qname(&name, "element")?;
248
249                    // ── 1. Open a namespace frame, parse xmlns decls into it. ──
250                    self.state.ns_frames.push(self.state.ns_bindings.len());
251                    // Two-pass over attrs is awkward with the drain pattern, so
252                    // capture them into a small Vec first.  The buffer is reused
253                    // across calls in the next `next_into`.
254                    let attrs: Vec<(String, String)> = self.attr_buf.drain(..)
255                        .map(|a| (a.name.to_owned(), a.value.into_owned()))
256                        .collect();
257                    for (an, av) in &attrs {
258                        if an == "xmlns" {
259                            self.state.ns_bindings.push((None, av.clone()));
260                        } else if let Some(local) = an.strip_prefix("xmlns:") {
261                            validate_xmlns_decl(local, av)?;
262                            if local == "xml" { continue; }  // legal no-op redeclare
263                            self.state.ns_bindings.push((Some(local.to_string()), av.clone()));
264                        }
265                    }
266
267                    // ── 2. Should this element start (or continue) an emission? ──
268                    let starts_emission = self.state.current.is_none() && self.element_matches();
269
270                    if starts_emission {
271                        // Spin up a fresh arena.
272                        let mut emis = Emission {
273                            builder:     DocumentBuilder::new(),
274                            start_depth: self.state.sax_depth,
275                            stack:       Vec::new(),
276                            ns_cache:    FxHashMap::default(),
277                        };
278                        let el_ptr = build_element_with_ns(
279                            &mut emis, &name, &attrs, &self.state.ns_bindings,
280                        )?;
281                        emis.stack.push(el_ptr);
282                        self.state.current = Some(emis);
283                    } else if let Some(emis) = self.state.current.as_mut() {
284                        let el_ptr = build_element_with_ns(
285                            emis, &name, &attrs, &self.state.ns_bindings,
286                        )?;
287                        // SAFETY: pointers point into emis.builder (still alive).
288                        let parent: &Node<'_> = unsafe { unerase(*emis.stack.last().unwrap()) };
289                        let el:     &Node<'_> = unsafe { unerase(el_ptr) };
290                        emis.builder.append_child(parent, el);
291                        emis.stack.push(el_ptr);
292                    } else {
293                        // Outside any emission — still validate the element's QName
294                        // namespace resolution (so undeclared prefixes are caught early).
295                        validate_element_ns(&name, &attrs, &self.state.ns_bindings)?;
296                    }
297                }
298
299                EventInto::EndElement { name: _ } => {
300                    self.state.name_stack.pop();
301                    // Pop this element's namespace frame.
302                    if let Some(frame_start) = self.state.ns_frames.pop() {
303                        self.state.ns_bindings.truncate(frame_start);
304                    }
305                    let closed_depth = self.state.sax_depth;
306                    self.state.sax_depth -= 1;
307
308                    if let Some(emis) = self.state.current.as_mut() {
309                        if emis.start_depth == closed_depth {
310                            // Closing the emission anchor — ship it.
311                            let emis = self.state.current.take().unwrap();
312                            // SAFETY: stack[0] is the root of this subtree, still in `emis.builder`.
313                            let root: &Node<'_> = unsafe { unerase(emis.stack[0]) };
314                            emis.builder.set_root(root);
315                            return Ok(Some(emis.builder.build()));
316                        } else {
317                            emis.stack.pop();
318                        }
319                    }
320                }
321
322                EventInto::Text(t) => {
323                    if let Some(emis) = self.state.current.as_mut() {
324                        let parent_ptr = *emis.stack.last().unwrap();
325                        let s    = emis.builder.alloc_str(&t);
326                        let node = emis.builder.new_text(s);
327                        // SAFETY: parent_ptr points into emis.builder's arena.
328                        let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
329                        emis.builder.append_child(parent, node);
330                    }
331                }
332                EventInto::CData(t) => {
333                    if let Some(emis) = self.state.current.as_mut() {
334                        let parent_ptr = *emis.stack.last().unwrap();
335                        let s    = emis.builder.alloc_str(&t);
336                        let node = emis.builder.new_cdata(s);
337                        let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
338                        emis.builder.append_child(parent, node);
339                    }
340                }
341                EventInto::Comment(t) => {
342                    if let Some(emis) = self.state.current.as_mut() {
343                        let parent_ptr = *emis.stack.last().unwrap();
344                        let s    = emis.builder.alloc_str(&t);
345                        let node = emis.builder.new_comment(s);
346                        let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
347                        emis.builder.append_child(parent, node);
348                    }
349                }
350                EventInto::Pi { target, content } => {
351                    if let Some(emis) = self.state.current.as_mut() {
352                        let parent_ptr = *emis.stack.last().unwrap();
353                        let t    = emis.builder.alloc_str(&target);
354                        let c    = if content.is_empty() { None } else { Some(&*emis.builder.alloc_str(&content)) };
355                        let node = emis.builder.new_pi(t, c);
356                        let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
357                        emis.builder.append_child(parent, node);
358                    }
359                }
360                EventInto::EntityRef { name } => {
361                    if let Some(emis) = self.state.current.as_mut() {
362                        let parent_ptr = *emis.stack.last().unwrap();
363                        let n  = emis.builder.alloc_str(&name);
364                        // Reconstruct the literal `&name;` source form
365                        // for round-trip serialization.
366                        let lit = format!("&{name};");
367                        let c  = emis.builder.alloc_str(&lit);
368                        let node = emis.builder.new_entity_ref(n, c);
369                        let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
370                        emis.builder.append_child(parent, node);
371                    }
372                }
373
374                EventInto::Eof => return Ok(None),
375            }
376        }
377    }
378
379    /// Does the just-opened element (whose name+attrs are already on the state
380    /// stacks / `attr_buf`) satisfy the emit predicate?
381    fn element_matches(&self) -> bool {
382        match &self.emit {
383            EmitMode::None       => false,
384            // Depth(d) emits when post-pop ancestor stack length == d.  At
385            // StartElement time, sax_depth already reflects the just-opened
386            // element, so the element is at depth `sax_depth`.  Its parent
387            // depth (post-pop) is `sax_depth - 1`.  Match when that equals d.
388            EmitMode::Depth(d)   => self.state.sax_depth.saturating_sub(1) == *d,
389            EmitMode::Path(path) => path.len() == self.state.name_stack.len()
390                && self.state.name_stack.iter().zip(path.iter())
391                    .all(|(a, b)| a == b),
392            EmitMode::When(pred) => pred(&self.state.name_stack),
393        }
394    }
395}
396
397// ── namespace helpers (per-emission) ────────────────────────────────────────
398
399/// Build an arena element with its full namespace state resolved against
400/// `global_ns_bindings`.  Caches Namespace allocations within `emis.ns_cache`.
401///
402/// Returns a type-erased pointer to the freshly-allocated element so the
403/// caller can continue using `emis` afterwards without conflicting borrows.
404fn build_element_with_ns(
405    emis:               &mut Emission,
406    name:               &str,
407    attrs:              &[(String, String)],
408    global_ns_bindings: &[(Option<String>, String)],
409) -> Result<*const ()> {
410    // Split-borrow: `builder` (shared) and `ns_cache` (exclusive) come from
411    // different fields, so the borrow checker tracks them independently —
412    // letting us hold a long-lived borrow on `builder` (via `el`) while still
413    // mutating `ns_cache`.
414    let Emission { builder, ns_cache, .. } = emis;
415
416    let aname = builder.alloc_str(name);
417    let el    = builder.new_element(aname);
418
419    // Resolve element's QName.
420    if let Some((p, h)) = resolve_qname(name, global_ns_bindings, /*is_attribute=*/ false)? {
421        let ns_ptr = lookup_or_alloc_ns(builder, ns_cache, p, h);
422        // SAFETY: ns_ptr came from builder.new_namespace; same lifetime as el.
423        let ns: &Namespace<'_> = unsafe { &*(ns_ptr as *const Namespace<'_>) };
424        el.namespace.set(Some(ns));
425    }
426
427    // Materialize attributes; resolve namespaces; check duplicate-by-expanded-name.
428    let mut seen: FxHashSet<(String, String)> = FxHashSet::default();
429    for (an, av) in attrs {
430        let aname  = builder.alloc_str(an);
431        let avalue = builder.alloc_str(av);
432        let attr   = builder.new_attribute(aname, avalue);
433        builder.append_attribute(el, attr);
434
435        if an == "xmlns" || an.starts_with("xmlns:") {
436            continue;
437        }
438        validate_qname(an, "attribute")?;
439        if let Some((p, h)) = resolve_qname(an, global_ns_bindings, /*is_attribute=*/ true)? {
440            let ns_ptr = lookup_or_alloc_ns(builder, ns_cache, p, h);
441            let ns: &Namespace<'_> = unsafe { &*(ns_ptr as *const Namespace<'_>) };
442            attr.namespace.set(Some(ns));
443        }
444        // Duplicate-by-expanded-name check (URI + local part).
445        let ns_uri = attr.namespace.get().map(|n| n.href()).unwrap_or("");
446        let local  = an.rfind(':').map(|i| &an[i+1..]).unwrap_or(an);
447        if !seen.insert((ns_uri.to_string(), local.to_string())) {
448            return Err(ns_err(format!(
449                "duplicate attribute '{local}' in namespace '{ns_uri}' after namespace expansion"
450            )));
451        }
452    }
453
454    Ok(erase(el))
455}
456
457/// Look up an arena Namespace for `(prefix, href)` in this emission's cache,
458/// allocating one if it doesn't exist.  Split-borrow form — takes `builder`
459/// and `ns_cache` separately so it can be called while another borrow on
460/// `Emission.builder` is active.
461fn lookup_or_alloc_ns(
462    builder:  &DocumentBuilder,
463    ns_cache: &mut FxHashMap<(Option<String>, String), *const ()>,
464    prefix:   Option<&str>,
465    href:     &str,
466) -> *const () {
467    let key = (prefix.map(str::to_owned), href.to_owned());
468    if let Some(&ptr) = ns_cache.get(&key) {
469        return ptr;
470    }
471    let pref_arena = prefix.map(|p| builder.alloc_str(p));
472    let href_arena = builder.alloc_str(href);
473    let ns         = builder.new_namespace(pref_arena, href_arena);
474    let ptr        = ns as *const Namespace<'_> as *const ();
475    ns_cache.insert(key, ptr);
476    ptr
477}
478
479/// Resolve a QName against the global namespace scope.  Returns
480/// `Some((prefix, href))` for any element/attribute that maps to a real
481/// namespace; `None` for in-no-namespace (unprefixed elements without a
482/// default ns, or unprefixed attributes).
483fn resolve_qname<'a>(
484    qname:        &'a str,
485    bindings:     &'a [(Option<String>, String)],
486    is_attribute: bool,
487) -> Result<Option<(Option<&'a str>, &'a str)>> {
488    if let Some(colon) = qname.find(':') {
489        let prefix = &qname[..colon];
490        match lookup_prefix(Some(prefix), bindings) {
491            Some(href) => Ok(Some((Some(prefix), href))),
492            None       => Err(ns_err(format!("undeclared namespace prefix '{prefix}' in '{qname}'"))),
493        }
494    } else if is_attribute {
495        Ok(None)
496    } else {
497        match lookup_prefix(None, bindings) {
498            Some(href) if !href.is_empty() => Ok(Some((None, href))),
499            _                              => Ok(None),
500        }
501    }
502}
503
504/// Lookup `prefix` (None = default ns) in the global scope, innermost wins.
505fn lookup_prefix<'a>(
506    prefix:   Option<&str>,
507    bindings: &'a [(Option<String>, String)],
508) -> Option<&'a str> {
509    for (p, h) in bindings.iter().rev() {
510        if p.as_deref() == prefix {
511            return Some(h);
512        }
513    }
514    None
515}
516
517/// Validate namespace resolution for an element we won't materialize (we're
518/// outside any emission).  Catches undeclared prefixes early so the streaming
519/// parser fails fast rather than silently skipping malformed content.
520fn validate_element_ns(
521    name:     &str,
522    attrs:    &[(String, String)],
523    bindings: &[(Option<String>, String)],
524) -> Result<()> {
525    validate_qname(name, "element")?;
526    let _ = resolve_qname(name, bindings, false)?;
527    for (an, _av) in attrs {
528        if an == "xmlns" || an.starts_with("xmlns:") { continue; }
529        validate_qname(an, "attribute")?;
530        let _ = resolve_qname(an, bindings, true)?;
531    }
532    Ok(())
533}
534
535// ── tests ───────────────────────────────────────────────────────────────────
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use sup_xml_tree::dom::NodeKind;
541
542    fn collect_names(mut sp: StreamParser) -> Vec<String> {
543        let mut out = Vec::new();
544        while let Some(doc) = sp.next().unwrap() {
545            out.push(doc.root().name().to_string());
546        }
547        out
548    }
549
550    // ── emit_at_depth ───────────────────────────────────────────────────
551
552    #[test]
553    fn depth_one_matches_children_of_root() {
554        let xml = "<r><a/><b/><c/></r>";
555        let sp = StreamParser::from_str(xml).emit_at_depth(1);
556        assert_eq!(collect_names(sp), vec!["a", "b", "c"]);
557    }
558
559    #[test]
560    fn depth_zero_emits_root() {
561        let xml = "<root><a/><b/></root>";
562        let mut sp = StreamParser::from_str(xml).emit_at_depth(0);
563        let doc = sp.next().unwrap().expect("should yield root");
564        assert_eq!(doc.root().name(), "root");
565        assert_eq!(doc.root().children().count(), 2);
566        assert!(sp.next().unwrap().is_none());
567    }
568
569    #[test]
570    fn depth_two_matches_grandchildren() {
571        let xml = "<r><a><b/><b/></a><a><b/></a></r>";
572        let sp = StreamParser::from_str(xml).emit_at_depth(2);
573        assert_eq!(collect_names(sp), vec!["b", "b", "b"]);
574    }
575
576    #[test]
577    fn no_emit_mode_yields_nothing() {
578        let mut sp = StreamParser::from_str("<r><a/></r>");
579        assert!(sp.next().unwrap().is_none());
580    }
581
582    // ── emit_at_path ────────────────────────────────────────────────────
583
584    #[test]
585    fn path_root_anchored() {
586        // Nested <item> inside an <item> must NOT match — its ancestors
587        // are rss/channel/item, not rss/channel.
588        let xml = "<rss><channel>\
589                     <item><id>1</id></item>\
590                     <item><id>2</id><item><id>2a</id></item></item>\
591                   </channel></rss>";
592        let sp = StreamParser::from_str(xml)
593            .emit_at_path(&["rss", "channel", "item"]);
594        assert_eq!(collect_names(sp), vec!["item", "item"]);
595    }
596
597    #[test]
598    fn path_wrong_root_yields_nothing() {
599        let xml = "<feed><channel><item/></channel></feed>";
600        let mut sp = StreamParser::from_str(xml)
601            .emit_at_path(&["rss", "channel", "item"]);
602        assert!(sp.next().unwrap().is_none());
603    }
604
605    #[test]
606    fn path_single_element_emits_root() {
607        let xml = "<root><a/></root>";
608        let mut sp = StreamParser::from_str(xml).emit_at_path(&["root"]);
609        let doc = sp.next().unwrap().unwrap();
610        assert_eq!(doc.root().name(), "root");
611        assert!(sp.next().unwrap().is_none());
612    }
613
614    // ── emit_when (predicate mode) ─────────────────────────────────────
615
616    /// Predicate over the just-opened element's name matches any
617    /// `<item>` anywhere — different depths, different ancestors.
618    /// `emit_at_path` couldn't express this; `emit_at_depth` would
619    /// over-emit.
620    #[test]
621    fn predicate_matches_by_name_across_nesting_levels() {
622        let xml = r#"<root>
623            <section><item>one</item></section>
624            <wrapper><inner><item>two</item></inner></wrapper>
625            <thing/>
626        </root>"#;
627        let sp = StreamParser::from_str(xml)
628            .emit_when(|chain| chain.last().map(String::as_str) == Some("item"));
629        let names: Vec<String> = {
630            let mut sp = sp;
631            let mut out = Vec::new();
632            while let Some(d) = sp.next().unwrap() {
633                out.push(d.root().text_content().unwrap_or_default().to_string());
634            }
635            out
636        };
637        assert_eq!(names, vec!["one", "two"],
638            "predicate should pick both <item>s irrespective of depth");
639    }
640
641    /// Predicate can reach into the full ancestor chain.  Matching
642    /// "any `book` whose immediate parent is `library`" excludes a
643    /// `book` nested inside `archive/library/book/book`.
644    #[test]
645    fn predicate_can_inspect_ancestor_chain() {
646        let xml = r#"<archive>
647            <library><book>L1</book></library>
648            <library><book>L2</book></library>
649            <library><book>L3<book>nested</book></book></library>
650        </archive>"#;
651        let sp = StreamParser::from_str(xml).emit_when(|chain| {
652            chain.len() >= 2
653                && chain.last().map(String::as_str)  == Some("book")
654                && chain.get(chain.len() - 2).map(String::as_str) == Some("library")
655        });
656        let texts: Vec<String> = {
657            let mut sp = sp;
658            let mut out = Vec::new();
659            while let Some(d) = sp.next().unwrap() {
660                let t = d.root().text_content().unwrap_or_default().to_string();
661                out.push(t);
662            }
663            out
664        };
665        // The nested `<book>nested</book>` has ancestor chain
666        // archive/library/book/book — last 2 are book/book, not
667        // library/book — so it should be skipped.
668        assert_eq!(texts.len(), 3,
669            "expected exactly the three immediate-child <book>s, got {texts:?}");
670    }
671
672    /// Predicate mode plays well with namespace resolution — emitted
673    /// subtrees carry resolved Namespace pointers just like the
674    /// fixed-depth modes.
675    #[test]
676    fn predicate_emission_preserves_namespace_resolution() {
677        let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom">
678            <entry><title>t1</title></entry>
679            <entry><title>t2</title></entry>
680        </feed>"#;
681        let mut sp = StreamParser::from_str(xml)
682            .emit_when(|chain| chain.last().map(String::as_str) == Some("entry"));
683        let entry = sp.next().unwrap().expect("should yield entry");
684        let ns = entry.root().namespace.get()
685            .expect("entry must inherit feed's default namespace");
686        assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
687    }
688
689    // ── subtree contents ────────────────────────────────────────────────
690
691    #[test]
692    fn emitted_subtree_contains_children() {
693        let xml = "<r><item><title>X</title><body>hello</body></item></r>";
694        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
695        let item = sp.next().unwrap().unwrap();
696        let root = item.root();
697        assert_eq!(root.name(), "item");
698        let title = root.find_child("title").unwrap();
699        assert_eq!(title.text_content(), Some("X"));
700        let body = root.find_child("body").unwrap();
701        assert_eq!(body.text_content(), Some("hello"));
702    }
703
704    #[test]
705    fn emitted_subtree_contains_attributes() {
706        let xml = r#"<r><page id="1" lang="en">x</page></r>"#;
707        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
708        let page = sp.next().unwrap().unwrap();
709        let pairs: Vec<(&str, &str)> = page.root().attributes()
710            .map(|a| (a.name(), a.value())).collect();
711        assert_eq!(pairs, vec![("id", "1"), ("lang", "en")]);
712    }
713
714    #[test]
715    fn mixed_content_preserved() {
716        let xml = "<r><item>before<!-- c --><b>x</b><![CDATA[raw]]>after</item></r>";
717        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
718        let item = sp.next().unwrap().unwrap();
719        let kinds: Vec<NodeKind> = item.root().children().map(|c| c.kind).collect();
720        assert_eq!(kinds, vec![
721            NodeKind::Text, NodeKind::Comment, NodeKind::Element,
722            NodeKind::CData, NodeKind::Text,
723        ]);
724    }
725
726    #[test]
727    fn entity_in_attr_and_text_expanded() {
728        let xml = r#"<r><x v="a&amp;b">c&lt;d</x></r>"#;
729        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
730        let x = sp.next().unwrap().unwrap();
731        assert_eq!(x.root().attributes().next().unwrap().value(), "a&b");
732        assert_eq!(x.root().text_content(), Some("c<d"));
733    }
734
735    // ── streaming property ──────────────────────────────────────────────
736
737    #[test]
738    fn many_emissions_independent_arenas() {
739        // Each emitted Document has its own Bump.  Dropping one shouldn't
740        // affect any other.  We hold all in a Vec and verify they're distinct.
741        let mut xml = String::from("<r>");
742        for i in 0..50 { xml.push_str(&format!("<i>{i}</i>")); }
743        xml.push_str("</r>");
744
745        let mut sp = StreamParser::from_str(&xml).emit_at_depth(1);
746        let mut docs = Vec::new();
747        while let Some(d) = sp.next().unwrap() { docs.push(d); }
748        assert_eq!(docs.len(), 50);
749        // Each has its own arena — bytes per-doc reflect just one <i>N</i>.
750        for d in &docs {
751            assert!(d.memory_bytes() > 0);
752            assert!(d.memory_bytes() < 4096, "single-item arena should be small");
753        }
754        // Drop docs[0] doesn't invalidate docs[1] etc.
755        let removed = docs.remove(0);
756        drop(removed);
757        assert_eq!(docs[0].root().name(), "i");
758    }
759
760    #[test]
761    fn eof_then_none_idempotent() {
762        let mut sp = StreamParser::from_str("<r><a/></r>").emit_at_depth(1);
763        assert!(sp.next().unwrap().is_some());
764        assert!(sp.next().unwrap().is_none());
765        assert!(sp.next().unwrap().is_none());
766    }
767
768    #[test]
769    fn errors_propagate() {
770        let mut sp = StreamParser::from_str("<r><a></b></r>").emit_at_depth(1);
771        assert!(sp.next().is_err());
772    }
773
774    #[test]
775    fn prolog_events_are_silently_discarded() {
776        let xml = "<?xml version='1.0'?><!-- pre --><?stylesheet?><r><a/></r>";
777        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
778        let a = sp.next().unwrap().unwrap();
779        assert_eq!(a.root().name(), "a");
780        assert!(sp.next().unwrap().is_none());
781    }
782
783    // ── namespace resolution ────────────────────────────────────────────
784
785    #[test]
786    fn ns_inherited_from_ancestor_above_emit_boundary() {
787        // <feed xmlns="..."> declares default ns; emitted <entry> inherits it.
788        let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom"><entry><title>X</title></entry></feed>"#;
789        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
790        let entry = sp.next().unwrap().unwrap();
791        let ns = entry.root().namespace.get().expect("entry must inherit feed's default ns");
792        assert!(ns.prefix.is_none());
793        assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
794        // <title> inside entry also resolves to the same ns
795        let title = entry.root().find_child("title").unwrap();
796        assert_eq!(title.namespace.get().unwrap().href(), "http://www.w3.org/2005/Atom");
797    }
798
799    #[test]
800    fn ns_prefix_inherited_from_ancestor() {
801        let xml = r#"<rss xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><dc:title>X</dc:title></channel></rss>"#;
802        let mut sp = StreamParser::from_str(xml)
803            .emit_at_path(&["rss", "channel"]);
804        let channel = sp.next().unwrap().unwrap();
805        let title = channel.root().find_child("dc:title").unwrap();
806        let ns = title.namespace.get().unwrap();
807        assert_eq!(ns.prefix(), Some("dc"));
808        assert_eq!(ns.href(),   "http://purl.org/dc/elements/1.1/");
809    }
810
811    #[test]
812    fn ns_declared_inside_emission() {
813        // Namespace declared on the emitted element itself.
814        let xml = r#"<feed><entry xmlns:atom="http://www.w3.org/2005/Atom" atom:rel="self"/></feed>"#;
815        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
816        let entry = sp.next().unwrap().unwrap();
817        let rel = entry.root().attributes().find(|a| a.name() == "atom:rel").unwrap();
818        let ns = rel.namespace.get().unwrap();
819        assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
820    }
821
822    #[test]
823    fn ns_xml_prefix_resolved_inside_emission() {
824        let xml = r#"<r><item xml:lang="en"/></r>"#;
825        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
826        let item = sp.next().unwrap().unwrap();
827        let lang = item.root().attributes().next().unwrap();
828        assert_eq!(lang.namespace.get().unwrap().href(),
829                   "http://www.w3.org/XML/1998/namespace");
830    }
831
832    #[test]
833    fn ns_undeclared_prefix_above_emit_boundary_errors() {
834        // Undeclared prefix outside the emitted region — streaming parser
835        // must catch it (fail-fast) even though it's not materializing this element.
836        let xml = r#"<r><foo:bad/><item/></r>"#;
837        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
838        // The first emit fails when we hit <foo:bad/>.
839        assert!(sp.next().is_err());
840    }
841
842    #[test]
843    fn ns_undeclared_prefix_inside_emission_errors() {
844        let xml = r#"<r><item><bad:thing/></item></r>"#;
845        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
846        assert!(sp.next().is_err());
847    }
848
849    #[test]
850    fn ns_cache_dedups_repeated_prefix_uses() {
851        // Two children using the same prefix should share one Namespace allocation
852        // in the emission's arena.  We can't directly assert pointer identity from
853        // outside, but we can assert that arena bytes don't grow linearly with
854        // repeated uses (compared to a single use).
855        let single = r#"<r><item><a><x:c xmlns:x="http://x.com/"/></a></item></r>"#;
856        let many = r#"<r><item><a xmlns:x="http://x.com/"><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/></a></item></r>"#;
857
858        let mut sp1 = StreamParser::from_str(single).emit_at_depth(1);
859        let mut sp2 = StreamParser::from_str(many).emit_at_depth(1);
860        let d1 = sp1.next().unwrap().unwrap();
861        let d2 = sp2.next().unwrap().unwrap();
862        // d2 should be slightly larger (more nodes + their attrs/strings) but not
863        // grow by ~8 Namespace allocations.  Allow generous slack — just verify
864        // it's not catastrophically larger.
865        let ratio = d2.memory_bytes() as f64 / d1.memory_bytes() as f64;
866        assert!(ratio < 4.0, "8× uses shouldn't bloat 8× — got ratio {ratio:.2}");
867    }
868
869    #[test]
870    fn ns_override_in_emitted_subtree() {
871        // Outer default ns overridden inside the emitted subtree.
872        let xml = r#"<root xmlns="http://outer.com/"><item><inner xmlns="http://inner.com/"/></item></root>"#;
873        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
874        let item = sp.next().unwrap().unwrap();
875        assert_eq!(item.root().namespace.get().unwrap().href(), "http://outer.com/");
876        let inner = item.root().find_child("inner").unwrap();
877        assert_eq!(inner.namespace.get().unwrap().href(), "http://inner.com/");
878    }
879
880    #[test]
881    fn ns_unprefixed_attr_not_in_default_ns() {
882        // xmlns="..." default ns applies to elements but NOT unprefixed attrs.
883        let xml = r#"<r xmlns="http://ex.com/"><item id="1"/></r>"#;
884        let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
885        let item = sp.next().unwrap().unwrap();
886        assert!(item.root().namespace.get().is_some());
887        let id = item.root().attributes().next().unwrap();
888        assert!(id.namespace.get().is_none(), "default ns must not apply to unprefixed attrs");
889    }
890}