use quick_xml::events::Event;
use crate::error::{Error, Result};
pub struct Reader<'a> {
inner: quick_xml::Reader<&'a [u8]>,
}
impl<'a> Reader<'a> {
pub fn from_xml_str(xml: &'a str) -> Self {
Self {
inner: quick_xml::Reader::from_str(xml),
}
}
pub fn read_event(&mut self) -> Result<Event<'a>> {
self.inner.read_event().map_err(Error::from)
}
}
pub fn decode_text(text: &quick_xml::events::BytesText<'_>) -> Result<String> {
let decoded = text.decode()?;
let unescaped = quick_xml::escape::unescape(&decoded)?;
Ok(unescaped.into_owned())
}
pub fn decode_general_ref(reference: &quick_xml::events::BytesRef<'_>) -> Result<Option<String>> {
if let Some(ch) = reference.resolve_char_ref()? {
return Ok(Some(ch.to_string()));
}
let name = reference.decode()?;
Ok(quick_xml::escape::resolve_xml_entity(&name).map(str::to_string))
}
pub fn decode_attribute_value(value: &[u8]) -> Result<Option<String>> {
let Ok(text) = std::str::from_utf8(value) else {
return Ok(None);
};
let unescaped = quick_xml::escape::unescape(text)?;
Ok(Some(unescaped.into_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_a_simple_element_with_text() {
let mut reader = Reader::from_xml_str("<a>hello</a>");
assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
match reader.read_event().unwrap() {
Event::Text(text) => assert_eq!(text.decode().unwrap(), "hello"),
other => panic!("expected a Text event, got {other:?}"),
}
assert!(matches!(reader.read_event().unwrap(), Event::End(_)));
assert!(matches!(reader.read_event().unwrap(), Event::Eof));
}
#[test]
fn a_text_node_with_entities_arrives_as_interleaved_text_and_generalref_events() {
let mut reader = Reader::from_xml_str(
"<a>Bells & whistles, 'quoted' and <tagged></a>",
);
reader.read_event().unwrap();
let mut reconstructed = String::new();
loop {
match reader.read_event().unwrap() {
Event::Text(text) => reconstructed.push_str(&decode_text(&text).unwrap()),
Event::GeneralRef(reference) => {
if let Some(resolved) = decode_general_ref(&reference).unwrap() {
reconstructed.push_str(&resolved);
}
}
Event::End(_) => break,
other => panic!("unexpected event: {other:?}"),
}
}
assert_eq!(reconstructed, "Bells & whistles, 'quoted' and <tagged>");
}
#[test]
fn decode_general_ref_resolves_numeric_character_references() {
let mut reader = Reader::from_xml_str("<a>AB</a>");
reader.read_event().unwrap();
let mut resolved = String::new();
loop {
match reader.read_event().unwrap() {
Event::GeneralRef(reference) => {
resolved.push_str(&decode_general_ref(&reference).unwrap().unwrap())
}
Event::End(_) => break,
other => panic!("unexpected event: {other:?}"),
}
}
assert_eq!(resolved, "AB");
}
#[test]
fn decode_attribute_value_unescapes_entities() {
let mut reader = Reader::from_xml_str(r#"<a k="Bells & whistles"/>"#);
match reader.read_event().unwrap() {
Event::Empty(start) => {
let attribute = start.attributes().next().unwrap().unwrap();
assert_eq!(attribute.value.as_ref(), b"Bells & whistles");
assert_eq!(
decode_attribute_value(attribute.value.as_ref())
.unwrap()
.as_deref(),
Some("Bells & whistles")
);
}
other => panic!("expected an Empty event, got {other:?}"),
}
}
#[test]
fn reads_attributes_on_an_empty_element() {
let mut reader = Reader::from_xml_str(r#"<a k="v"/>"#);
match reader.read_event().unwrap() {
Event::Empty(start) => {
let attribute = start.attributes().next().unwrap().unwrap();
assert_eq!(attribute.key.as_ref(), b"k");
assert_eq!(attribute.value.as_ref(), b"v");
}
other => panic!("expected an Empty event, got {other:?}"),
}
}
#[test]
fn reports_malformed_xml_as_an_error() {
let mut reader = Reader::from_xml_str("<a><b></a>");
assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
assert!(reader.read_event().is_err());
}
}