#[cfg(any(feature = "xml", feature = "office"))]
use std::borrow::Cow;
#[cfg(any(feature = "xml", feature = "office"))]
#[inline]
pub(crate) fn xml_tag_name(name: &[u8]) -> Cow<'_, str> {
String::from_utf8_lossy(name)
}
#[cfg(any(feature = "xml", feature = "office"))]
pub(crate) struct EntityReader<'x> {
reader: quick_xml::Reader<&'x [u8]>,
pending: Option<quick_xml::events::Event<'x>>,
}
#[cfg(any(feature = "xml", feature = "office"))]
impl<'x> EntityReader<'x> {
pub(crate) fn from_str(content: &'x str) -> Self {
Self {
reader: quick_xml::Reader::from_str(content),
pending: None,
}
}
pub(crate) fn from_bytes(content: &'x [u8]) -> Self {
Self {
reader: quick_xml::Reader::from_reader(content),
pending: None,
}
}
pub(crate) fn config_mut(&mut self) -> &mut quick_xml::reader::Config {
self.reader.config_mut()
}
pub(crate) fn buffer_position(&self) -> u64 {
self.reader.buffer_position()
}
pub(crate) fn read_event(&mut self) -> quick_xml::Result<quick_xml::events::Event<'x>> {
use quick_xml::events::{BytesText, Event};
let first = match self.pending.take() {
Some(event) => event,
None => self.reader.read_event()?,
};
let mut text = match first {
Event::Text(t) => String::from_utf8_lossy(t.as_ref()).into_owned(),
Event::GeneralRef(r) => resolve_general_ref(&r),
other => return Ok(other),
};
loop {
match self.reader.read_event()? {
Event::Text(t) => text.push_str(&String::from_utf8_lossy(t.as_ref())),
Event::GeneralRef(r) => text.push_str(&resolve_general_ref(&r)),
other => {
self.pending = Some(other);
break;
}
}
}
Ok(Event::Text(BytesText::from_escaped(text)))
}
}
#[cfg(any(feature = "xml", feature = "office"))]
pub(crate) fn resolve_general_ref(reference: &quick_xml::events::BytesRef<'_>) -> String {
if let Ok(Some(ch)) = reference.resolve_char_ref() {
return ch.to_string();
}
let Ok(name) = reference.decode() else {
return String::new();
};
if let Some(resolved) = quick_xml::escape::resolve_predefined_entity(&name) {
return resolved.to_string();
}
if name.as_ref() == "nbsp" {
return "\u{00A0}".to_string();
}
String::new()
}
#[cfg(all(test, any(feature = "xml", feature = "office")))]
mod tests {
use super::*;
use quick_xml::events::{BytesRef, Event};
fn resolve(name: &str) -> String {
resolve_general_ref(&BytesRef::new(name))
}
#[test]
fn test_resolve_general_ref_predefined_entities() {
assert_eq!(resolve("amp"), "&");
assert_eq!(resolve("lt"), "<");
assert_eq!(resolve("gt"), ">");
assert_eq!(resolve("quot"), "\"");
assert_eq!(resolve("apos"), "'");
assert_eq!(resolve("nbsp"), "\u{00A0}");
}
#[test]
fn test_resolve_general_ref_character_references() {
assert_eq!(resolve("#8212"), "\u{2014}");
assert_eq!(resolve("#x2014"), "\u{2014}");
assert_eq!(resolve("#65"), "A");
}
#[test]
fn test_resolve_general_ref_unknown_entity_is_empty() {
assert_eq!(resolve("unknownentity"), "");
assert_eq!(resolve("#xZZ"), "");
assert_eq!(resolve("#1114112"), ""); }
#[test]
fn test_entity_reader_coalesces_text_and_references() {
let xml = "<root><a>Profits & losses</a><b>5>3</b><c/></root>";
let mut reader = EntityReader::from_str(xml);
let mut texts = Vec::new();
loop {
match reader.read_event().expect("valid XML") {
Event::Text(t) => texts.push(String::from_utf8_lossy(t.as_ref()).into_owned()),
Event::Eof => break,
_ => {}
}
}
assert_eq!(texts, vec!["Profits & losses", "5>3"]);
}
#[test]
fn test_entity_reader_preserves_non_text_events() {
let xml = "<root attr=\"v\">xAy<child/></root>";
let mut reader = EntityReader::from_str(xml);
let mut summary = Vec::new();
loop {
match reader.read_event().expect("valid XML") {
Event::Start(e) => summary.push(format!("start:{}", String::from_utf8_lossy(e.name().as_ref()))),
Event::Empty(e) => summary.push(format!("empty:{}", String::from_utf8_lossy(e.name().as_ref()))),
Event::End(e) => summary.push(format!("end:{}", String::from_utf8_lossy(e.name().as_ref()))),
Event::Text(t) => summary.push(format!("text:{}", String::from_utf8_lossy(t.as_ref()))),
Event::Eof => break,
_ => {}
}
}
assert_eq!(summary, vec!["start:root", "text:xAy", "empty:child", "end:root"]);
}
}