use std::collections::HashMap;
use crate::error::XmlError;
use crate::reader::{Attr, Event, XmlReader};
use crate::options::ParseOptions;
#[derive(Debug, Clone)]
pub enum IterEvent {
StartElement {
name: String,
attrs: Vec<(String, String)>,
path: String,
depth: usize,
},
EndElement {
name: String,
path: String,
depth: usize,
},
Text {
content: String,
path: String,
depth: usize,
},
CData {
content: String,
path: String,
depth: usize,
},
Comment {
content: String,
depth: usize,
},
Pi {
target: String,
data: String,
depth: usize,
},
}
pub struct Iterparse<'src> {
reader: XmlReader<'src>,
stack: Vec<StackFrame>,
done: bool,
}
struct StackFrame {
name: String,
children: HashMap<String, u32>,
}
impl<'src> Iterparse<'src> {
pub fn from_bytes(bytes: &'src [u8]) -> Result<Self, XmlError> {
let reader = XmlReader::from_bytes(bytes)?;
Ok(Self { reader, stack: Vec::new(), done: false })
}
pub fn from_bytes_with(bytes: &'src [u8], opts: &ParseOptions) -> Result<Self, XmlError> {
let reader = XmlReader::from_bytes(bytes)?.with_options(opts.clone());
Ok(Self { reader, stack: Vec::new(), done: false })
}
fn current_path(&self) -> String {
if self.stack.is_empty() { return "/".into(); }
let mut s = String::new();
let mut prev_children: Option<&HashMap<String, u32>> = None;
for f in &self.stack {
s.push('/');
s.push_str(&f.name);
if let Some(counters) = prev_children {
if let Some(&n) = counters.get(&f.name) {
if n >= 2 {
s.push_str(&format!("[{n}]"));
}
}
}
prev_children = Some(&f.children);
}
s
}
}
impl<'src> Iterator for Iterparse<'src> {
type Item = Result<IterEvent, XmlError>;
fn next(&mut self) -> Option<Self::Item> {
if self.done { return None; }
let depth = self.stack.len();
let ev = match self.reader.next() {
Ok(ev) => ev,
Err(e) => {
self.done = true;
return Some(Err(e));
}
};
match ev {
Event::Eof => {
self.done = true;
None
}
Event::StartElement(tag) => {
let name = tag.name().to_string();
let attrs: Vec<(String, String)> = tag.attrs()
.filter_map(|a: Result<Attr<'_>, _>| a.ok())
.map(|a| (a.name().to_string(), a.value().to_string()))
.collect();
if let Some(parent) = self.stack.last_mut() {
*parent.children.entry(name.clone()).or_insert(0) += 1;
}
self.stack.push(StackFrame {
name: name.clone(),
children: HashMap::new(),
});
let path = self.current_path();
let new_depth = self.stack.len();
Some(Ok(IterEvent::StartElement {
name, attrs, path, depth: new_depth,
}))
}
Event::EndElement(end) => {
let path = self.current_path();
let name = end.name().to_string();
self.stack.pop();
Some(Ok(IterEvent::EndElement { name, path, depth }))
}
Event::Text(t) => {
let path = self.current_path();
Some(Ok(IterEvent::Text {
content: t.as_str().to_string(),
path, depth,
}))
}
Event::CData(c) => {
let path = self.current_path();
Some(Ok(IterEvent::CData {
content: c.as_str().to_string(),
path, depth,
}))
}
Event::Comment(c) => Some(Ok(IterEvent::Comment {
content: c.as_str().to_string(),
depth,
})),
Event::Pi(p) => Some(Ok(IterEvent::Pi {
target: p.target().to_string(),
data: p.content().to_string(),
depth,
})),
Event::EntityRef(_) => {
self.next()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(xml: &[u8]) -> Vec<IterEvent> {
Iterparse::from_bytes(xml).unwrap()
.map(|r| r.unwrap())
.collect()
}
#[test]
fn yields_start_end_text() {
let events = collect(b"<r>hi</r>");
assert_eq!(events.len(), 3);
assert!(matches!(&events[0],
IterEvent::StartElement { name, .. } if name == "r"));
assert!(matches!(&events[1],
IterEvent::Text { content, .. } if content == "hi"));
assert!(matches!(&events[2],
IterEvent::EndElement { name, .. } if name == "r"));
}
#[test]
fn path_includes_sibling_index() {
let events = collect(
b"<r><a/><a><b/></a><a/></r>",
);
let mid_end = events.iter().find_map(|e| match e {
IterEvent::EndElement { name, path, .. }
if name == "a" && path.contains("[2]") => Some(path.clone()),
_ => None,
});
assert_eq!(mid_end.as_deref(), Some("/r/a[2]"));
}
#[test]
fn captures_attrs() {
let events = collect(br#"<r a="1" b="2"/>"#);
match &events[0] {
IterEvent::StartElement { attrs, .. } => {
assert_eq!(attrs.len(), 2);
assert_eq!(attrs[0], ("a".into(), "1".into()));
assert_eq!(attrs[1], ("b".into(), "2".into()));
}
_ => panic!(),
}
}
#[test]
fn malformed_input_surfaces_error_then_stops() {
let mut it = Iterparse::from_bytes(b"<r><unclosed>").unwrap();
let mut saw_err = false;
for ev in it.by_ref() {
if ev.is_err() { saw_err = true; break; }
}
assert!(saw_err);
assert!(it.next().is_none(), "iterator must stop after first error");
}
#[test]
fn comments_and_pis_dont_push_path() {
let events = collect(b"<r><!-- c --><?pi data?><a/></r>");
let a_start = events.iter().find_map(|e| match e {
IterEvent::StartElement { name, path, .. } if name == "a" => Some(path.clone()),
_ => None,
});
assert_eq!(a_start.as_deref(), Some("/r/a"));
}
}