use quick_xml::Reader;
use quick_xml::escape::unescape;
use quick_xml::events::{BytesRef, BytesText, Event};
use quick_xml::name::QName;
pub fn decode_escaped(text: &BytesText<'_>) -> String {
let Ok(raw) = text.decode() else {
return String::new();
};
match unescape(&raw) {
Ok(unescaped) => unescaped.into_owned(),
Err(_) => raw.into_owned(),
}
}
pub fn decode_plain(text: &BytesText<'_>) -> String {
text.decode().map(|c| c.into_owned()).unwrap_or_default()
}
pub fn resolve_entity(entity: &BytesRef<'_>) -> String {
let Ok(name) = entity.decode() else {
return String::new();
};
match unescape(&format!("&{name};")) {
Ok(resolved) => resolved.into_owned(),
Err(_) => format!("&{name};"),
}
}
pub fn read_element_text(reader: &mut Reader<&[u8]>, start_name: QName<'_>) -> String {
let end = start_name.as_ref().to_vec();
let mut out = String::new();
let mut buf = Vec::new();
let mut depth = 1u32;
let (trim_start, trim_end) = {
let config = reader.config_mut();
let previous = (config.trim_text_start, config.trim_text_end);
config.trim_text(false);
previous
};
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
if e.name().as_ref() == end {
depth += 1;
}
}
Ok(Event::End(ref e)) => {
if e.name().as_ref() == end {
depth -= 1;
if depth == 0 {
break;
}
}
}
Ok(Event::Text(ref e)) => out.push_str(&decode_plain(e)),
Ok(Event::CData(ref e)) => {
if let Ok(decoded) = e.decode() {
out.push_str(&decoded);
}
}
Ok(Event::GeneralRef(ref e)) => out.push_str(&resolve_entity(e)),
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buf.clear();
}
let config = reader.config_mut();
config.trim_text_start = trim_start;
config.trim_text_end = trim_end;
out
}
#[cfg(test)]
mod tests {
use super::*;
fn text_of(xml: &str) -> String {
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) if e.name().as_ref() == b"t" => {
return read_element_text(&mut reader, e.name());
}
Ok(Event::Eof) => return String::new(),
_ => {}
}
}
}
#[test]
fn entities_survive_text_accumulation() {
assert_eq!(text_of("<t>a & b</t>"), "a & b");
assert_eq!(text_of("<t><tag></t>"), "<tag>");
assert_eq!(text_of("<t>AB</t>"), "AB");
assert_eq!(text_of("<t>plain</t>"), "plain");
}
#[test]
fn whitespace_around_entities_is_kept_even_when_trimming() {
let mut reader = Reader::from_str("<t>Title & Co. <tagged></t>");
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
assert_eq!(
read_element_text(&mut reader, e.name()),
"Title & Co. <tagged>"
);
assert!(reader.config().trim_text_start);
return;
}
Ok(Event::Eof) => panic!("no start tag"),
_ => {}
}
}
}
#[test]
fn unknown_entity_is_preserved_verbatim() {
assert_eq!(text_of("<t>a b</t>"), "a b");
}
#[test]
fn read_text_span_is_unescaped() {
let xml = "<t>a & b</t>";
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let span = reader.read_text(e.name()).unwrap();
assert_eq!(decode_escaped(&span), "a & b");
return;
}
Ok(Event::Eof) => panic!("no start tag"),
_ => {}
}
}
}
#[test]
fn xml_text_handles_cdata_mixed_nested_and_general_refs() {
assert_eq!(
text_of("<t>start<![CDATA[<raw>]]><n>nested & text</n>end </t>"),
"start<raw>nested & textend "
);
}
}