#![allow(unused_variables)]
use alloc::{boxed::Box, collections::BTreeMap, string::String, vec::Vec};
use core::fmt;
#[cfg(feature = "std")]
use std::path::Path;
#[cfg(feature = "svg")]
pub mod svg;
fn decode_xml_entities(s: &str) -> std::borrow::Cow<'_, str> {
if !s.contains('&') {
return std::borrow::Cow::Borrowed(s);
}
decode_xml_entities_slow(s)
}
fn decode_xml_entities_slow(s: &str) -> std::borrow::Cow<'_, str> {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '&' {
let mut entity = String::new();
let mut found_semicolon = false;
while let Some(&next) = chars.peek() {
if next == ';' {
chars.next();
found_semicolon = true;
break;
}
if !next.is_alphanumeric() && next != '#' {
break;
}
entity.push(chars.next().unwrap());
if entity.len() > 10 {
break;
}
}
if found_semicolon {
match entity.as_str() {
"lt" => result.push('<'),
"gt" => result.push('>'),
"amp" => result.push('&'),
"apos" => result.push('\''),
"quot" => result.push('"'),
"nbsp" => result.push('\u{00A0}'),
s if s.starts_with('#') => {
let num_str = &s[1..];
let code_point = if num_str.starts_with('x') || num_str.starts_with('X') {
u32::from_str_radix(&num_str[1..], 16).ok()
} else {
num_str.parse::<u32>().ok()
};
if let Some(cp) = code_point {
if let Some(ch) = char::from_u32(cp) {
result.push(ch);
} else {
result.push('&');
result.push_str(&entity);
result.push(';');
}
} else {
result.push('&');
result.push_str(&entity);
result.push(';');
}
}
_ => {
result.push('&');
result.push_str(&entity);
result.push(';');
}
}
} else {
result.push('&');
result.push_str(&entity);
}
} else {
result.push(c);
}
}
std::borrow::Cow::Owned(result)
}
pub use azul_core::xml::*;
use azul_core::{dom::Dom, impl_from, styled_dom::StyledDom, window::StringPairVec};
#[cfg(feature = "parser")]
use azul_css::parser2::CssParseError;
use azul_css::{css::Css, AzString, OptionString, U8Vec};
use xmlparser::Tokenizer;
#[cfg(feature = "xml")]
#[must_use] pub fn domxml_from_str(xml: &str, component_map: &ComponentMap) -> DomXml {
let error_css = Css::empty();
let parsed = match parse_xml_string(xml) {
Ok(parsed) => parsed,
Err(e) => {
return DomXml {
parsed_dom: {
let mut dom = Dom::create_body()
.with_children(vec![Dom::create_text(format!("{e}"))].into());
StyledDom::create(&mut dom, error_css)
},
};
}
};
let parsed_dom = match str_to_dom(parsed.as_ref(), component_map, None) {
Ok(o) => o,
Err(e) => {
return DomXml {
parsed_dom: {
let mut dom = Dom::create_body()
.with_children(vec![Dom::create_text(format!("{e}"))].into());
StyledDom::create(&mut dom, error_css)
},
};
}
};
DomXml { parsed_dom }
}
#[allow(clippy::needless_pass_by_value)]
#[must_use] pub fn dom_from_parsed_xml(xml: Xml) -> Dom {
let component_map = ComponentMap::with_builtin();
match str_to_dom_unstyled(xml.root.as_ref(), &component_map) {
Ok(dom) => dom,
Err(e) => Dom::create_body().with_children(vec![Dom::create_text(format!("{e}"))].into()),
}
}
pub fn parse_xml_to_fast_dom(xml: &str) -> Result<azul_core::dom::FastDom, XmlError> {
let (fast_dom, _css) = parse_xml_to_fast_dom_with_css(xml)?;
Ok(fast_dom)
}
#[allow(clippy::cast_precision_loss)] pub fn parse_xml_to_styled_dom(xml: &str) -> Result<StyledDom, XmlError> {
static MEM_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let mem_on = *MEM_ENABLED.get_or_init(azul_core::profile::memory_enabled);
let rss0 = if mem_on { peak_rss_bytes() } else { 0 };
let (mut fast_dom, css) = parse_xml_to_fast_dom_with_css(xml)?;
if mem_on {
let rss1 = peak_rss_bytes();
eprintln!(
"[XML] tokenize+fast_dom : +{:.2} MiB",
(rss1.saturating_sub(rss0)) as f64 / 1024.0 / 1024.0,
);
}
let rss1 = if mem_on { peak_rss_bytes() } else { 0 };
if !css.is_empty() {
let combined_css = Css::new(css.into_iter()
.flat_map(|c| c.rules.into_library_owned_vec())
.collect());
fast_dom.css = vec![azul_core::dom::CssWithNodeId {
node_id: 0, css: combined_css,
}].into();
}
if mem_on {
let rss2 = peak_rss_bytes();
eprintln!(
"[XML] css attach : +{:.2} MiB",
(rss2.saturating_sub(rss1)) as f64 / 1024.0 / 1024.0,
);
}
crate::probe::hint_purge_allocator();
let rss2 = if mem_on { peak_rss_bytes() } else { 0 };
let styled = StyledDom::create_from_fast_dom(fast_dom);
crate::probe::hint_purge_allocator();
if mem_on {
let rss3 = peak_rss_bytes();
eprintln!(
"[XML] create_from_fast_dom : +{:.2} MiB",
(rss3.saturating_sub(rss2)) as f64 / 1024.0 / 1024.0,
);
}
Ok(styled)
}
#[cfg(all(unix, feature = "probe"))]
fn peak_rss_bytes() -> u64 {
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
return 0;
}
let ru = usage.ru_maxrss as u64;
#[cfg(target_os = "macos")]
{ ru }
#[cfg(not(target_os = "macos"))]
{ ru.saturating_mul(1024) }
}
#[cfg(not(all(unix, feature = "probe")))]
const fn peak_rss_bytes() -> u64 {
0
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn parse_xml_to_fast_dom_with_css(xml: &str) -> Result<(azul_core::dom::FastDom, Vec<Css>), XmlError> {
use xmlparser::{ElementEnd::{Open, Empty, Close}, Token::{ElementStart, Attribute, ElementEnd, Text}, Tokenizer};
use azul_core::dom::{NodeData, NodeType, IdOrClass, TabIndex};
use azul_core::xml::CompactDomBuilder;
const ESTIMATED_BYTES_PER_NODE: usize = 20;
const VOID_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
"param", "source", "track", "wbr",
];
fn lowercase_into(dst: &mut String, src: &str) {
dst.clear();
if src.bytes().all(|b| !b.is_ascii_uppercase()) {
dst.push_str(src);
} else {
dst.reserve(src.len());
for b in src.bytes() {
dst.push(b.to_ascii_lowercase() as char);
}
}
}
let xml = xml.strip_prefix('\u{FEFF}').unwrap_or(xml);
let mut xml = xml.trim();
if xml.starts_with("<?") {
if let Some(pos) = xml.find("?>") {
xml = &xml[(pos + 2)..];
}
}
let mut xml = xml.trim();
if xml.len() > 9 && xml.is_char_boundary(9) && xml[..9].to_ascii_lowercase().starts_with("<!doctype") {
if let Some(pos) = xml.find('>') {
xml = &xml[(pos + 1)..];
}
} else if xml.starts_with("<!--") {
if let Some(end) = xml.find("-->") {
xml = &xml[(end + 3)..];
xml = xml.trim();
}
}
let tokenizer = Tokenizer::from_fragment(xml, 0..xml.len());
let estimated_nodes = xml.len() / ESTIMATED_BYTES_PER_NODE;
let mut builder = CompactDomBuilder::with_capacity(estimated_nodes);
let mut collected_css: Vec<Css> = Vec::new();
let mut inside_style_tag = false;
let mut style_text = String::new();
let mut head_depth: usize = 0;
let mut current_tag: String = String::new();
let mut current_attrs: Vec<(String, String)> = Vec::new();
let mut pending_open = false;
let css_key_map = azul_css::props::property::get_css_key_map();
let mut str_arena = azul_css::corety::StringArena::new();
let finalize_open = |
builder: &mut CompactDomBuilder,
str_arena: &mut azul_css::corety::StringArena,
tag: &str,
attrs: &[(String, String)],
css_key_map: &azul_css::props::property::CssKeyMap,
| {
let node_type = tag_to_node_type(tag);
let mut nd = NodeData::create_node(node_type);
let mut attr_vec: Vec<azul_core::dom::AttributeType> = Vec::new();
for (key, value) in attrs {
match key.as_str() {
"id" => {
for id in value.split_whitespace() {
attr_vec.push(azul_core::dom::AttributeType::Id(str_arena.intern(id)));
}
}
"class" => {
for class in value.split_whitespace() {
attr_vec.push(azul_core::dom::AttributeType::Class(str_arena.intern(class)));
}
}
"focusable" => {
if let Some(f) = parse_bool(value.as_str()) {
nd.set_tab_index(if f { TabIndex::Auto } else { TabIndex::NoKeyboardFocus });
}
}
"tabindex" => {
if let Ok(ti) = value.parse::<isize>() {
match ti {
0 => nd.set_tab_index(TabIndex::Auto),
i if i > 0 => nd.set_tab_index(TabIndex::OverrideInParent(i as u32)),
_ => nd.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
}
"style" => {
let mut css_attrs = Vec::new();
for s in value.split(';') {
let mut s = s.split(':');
let Some(key) = s.next() else { continue };
let Some(val) = s.next() else { continue };
drop(azul_css::parser2::parse_css_declaration(
key.trim(), val.trim(),
azul_css::parser2::ErrorLocationRange::default(),
css_key_map, &mut Vec::new(), &mut css_attrs,
));
}
let props = css_attrs.into_iter().filter_map(|s| {
use azul_css::css::CssDeclaration;
use azul_css::dynamic_selector::CssPropertyWithConditions;
match s {
CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
CssDeclaration::Dynamic(_) => None,
}
}).collect::<Vec<_>>();
if !props.is_empty() {
nd.set_css_props(props.into());
}
}
"contenteditable" => {
if parse_bool(value.as_str()).unwrap_or(false) {
nd.set_contenteditable(true);
}
}
_ => {}
}
}
if !attr_vec.is_empty() {
nd.set_attributes(attr_vec.into());
}
builder.open_node(nd);
};
let mut last_was_void = false;
let mut tag_stack: Vec<String> = Vec::new();
for token in tokenizer {
let token = token.map_err(|e| XmlError::ParserError(translate_xmlparser_error(e)))?;
match token {
ElementStart { local, .. } => {
if pending_open {
let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
if current_tag == "head" { head_depth += 1; }
if head_depth == 0 {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
if is_void { builder.close_node(); }
}
if !is_void {
tag_stack.push(core::mem::take(&mut current_tag));
}
}
lowercase_into(&mut current_tag, local.as_str());
current_attrs.clear();
pending_open = true;
last_was_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
}
Attribute { local, value, .. } => {
current_attrs.push((local.to_string(), decode_xml_entities(value.as_str()).into_owned()));
}
ElementEnd { end: Open, .. } => {
if pending_open {
let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
if current_tag == "style" {
inside_style_tag = true;
style_text.clear();
}
if current_tag == "head" { head_depth += 1; }
if head_depth == 0 {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
if is_void { builder.close_node(); }
}
if !is_void {
tag_stack.push(core::mem::take(&mut current_tag));
}
pending_open = false;
}
}
ElementEnd { end: Empty, .. } => {
if pending_open {
if current_tag == "head" { head_depth += 1; }
if head_depth == 0 {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
builder.close_node();
}
if current_tag == "head" && head_depth > 0 { head_depth -= 1; }
pending_open = false;
}
}
ElementEnd { end: Close(_, close_value), .. } => {
if pending_open {
let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
if current_tag == "head" { head_depth += 1; }
if head_depth == 0 {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
if is_void { builder.close_node(); }
}
if !is_void {
tag_stack.push(core::mem::take(&mut current_tag));
}
pending_open = false;
}
let close_lower = close_value.as_str().to_ascii_lowercase();
let close_str = close_lower.as_str();
if VOID_ELEMENTS.contains(&close_str) {
continue;
}
if close_str == "style" && inside_style_tag {
if !style_text.is_empty() {
let parsed_css = Css::from_string(core::mem::take(&mut style_text).into());
collected_css.push(parsed_css);
}
inside_style_tag = false;
}
while let Some(top) = tag_stack.last() {
let is_match = top == close_str;
let was_head = top == "head";
let popped = tag_stack.pop().unwrap();
if popped == "head" && head_depth > 0 { head_depth -= 1; }
if head_depth == 0 && !was_head {
builder.close_node();
}
if is_match { break; }
}
}
Text { text } => {
if pending_open {
let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
if current_tag == "style" {
inside_style_tag = true;
style_text.clear();
}
if current_tag == "head" { head_depth += 1; }
if head_depth == 0 {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
if is_void { builder.close_node(); }
}
if !is_void {
tag_stack.push(current_tag.clone());
}
pending_open = false;
}
let text_str = text.as_str();
if !text_str.is_empty() {
if inside_style_tag {
style_text.push_str(text_str);
} else if head_depth == 0 {
let inside_body = tag_stack.iter().any(|t| t == "body");
if inside_body || !text_str.trim().is_empty() {
let decoded = decode_xml_entities(text_str);
builder.add_leaf(NodeData::create_text(str_arena.intern(&decoded)));
}
}
}
}
_ => {}
}
}
if pending_open {
finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
}
while tag_stack.pop().is_some() {
builder.close_node();
}
drop(str_arena);
Ok((builder.finish(), collected_css))
}
#[cfg(all(feature = "std", feature = "xml"))]
pub fn domxml_from_file<I: AsRef<Path>>(
file_path: I,
component_map: &ComponentMap,
) -> DomXml {
use std::fs;
let error_css = Css::empty();
let xml = match fs::read_to_string(file_path.as_ref()) {
Ok(xml) => xml,
Err(e) => {
return DomXml {
parsed_dom: {
let mut dom = Dom::create_body()
.with_children(
vec![Dom::create_text(format!(
"Error reading: \"{}\": {}",
file_path.as_ref().to_string_lossy(),
e
))]
.into(),
);
StyledDom::create(&mut dom, error_css)
},
};
}
};
domxml_from_str(&xml, component_map)
}
#[cfg(feature = "xml")]
#[allow(clippy::too_many_lines)] pub fn parse_xml_string(xml: &str) -> Result<Vec<XmlNodeChild>, XmlError> {
use xmlparser::{ElementEnd::{Empty, Close}, Token::{ElementStart, ElementEnd, Attribute, Text}, Tokenizer};
use self::XmlParseError::*;
const VOID_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
];
const AUTO_CLOSE_RULES: &[(&str, &[&str])] = &[
("li", &["li"]),
("td", &["td", "th", "tr"]),
("th", &["td", "th", "tr"]),
("tr", &["tr"]),
(
"p",
&[
"address",
"article",
"aside",
"blockquote",
"div",
"dl",
"fieldset",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"main",
"nav",
"ol",
"p",
"pre",
"section",
"table",
"ul",
],
),
("option", &["option", "optgroup"]),
("optgroup", &["optgroup"]),
("dd", &["dd", "dt"]),
("dt", &["dd", "dt"]),
];
let mut root_node = XmlNode::default();
let xml = xml.strip_prefix('\u{FEFF}').unwrap_or(xml);
let mut xml = xml.trim();
if xml.starts_with("<?") {
let pos = xml.find("?>").ok_or(XmlError::MalformedHierarchy(
MalformedHierarchyError {
expected: "<?xml".into(),
got: "?>".into(),
},
))?;
xml = &xml[(pos + 2)..];
}
let mut xml = xml.trim();
if xml.len() > 9 && xml.is_char_boundary(9) && xml[..9].to_ascii_lowercase().starts_with("<!doctype") {
let pos = xml.find('>').ok_or(XmlError::MalformedHierarchy(
MalformedHierarchyError {
expected: "<!DOCTYPE".into(),
got: ">".into(),
},
))?;
xml = &xml[(pos + 1)..];
} else if xml.starts_with("<!--") {
if let Some(end) = xml.find("-->") {
xml = &xml[(end + 3)..];
xml = xml.trim();
}
}
let tokenizer = Tokenizer::from_fragment(xml, 0..xml.len());
let mut node_stack: Vec<*mut XmlNode> = vec![&raw mut root_node];
let mut last_was_void = false;
for token in tokenizer {
let token = token.map_err(|e| XmlError::ParserError(translate_xmlparser_error(e)))?;
match token {
ElementStart { local, .. } => {
let tag_name = local.to_string();
let is_void_element = VOID_ELEMENTS.contains(&tag_name.as_str());
if last_was_void {
node_stack.pop();
last_was_void = false;
}
if node_stack.len() > 1 {
let current_element = unsafe { &*node_stack[node_stack.len() - 1] };
let current_tag = current_element.node_type.as_str();
for (element, closes_on) in AUTO_CLOSE_RULES {
if current_tag == *element && closes_on.contains(&tag_name.as_str()) {
node_stack.pop();
break;
}
}
}
if let Some(¤t_parent_ptr) = node_stack.last() {
let current_parent = unsafe { &mut *current_parent_ptr };
current_parent.children.push(XmlNodeChild::Element(XmlNode {
node_type: tag_name.into(),
attributes: StringPairVec::new().into(),
children: Vec::new().into(),
}));
let children_len = current_parent.children.len();
if let Some(XmlNodeChild::Element(ref mut new_child)) = current_parent.children.as_mut().get_mut(children_len - 1) {
node_stack.push(std::ptr::from_mut::<XmlNode>(new_child));
}
last_was_void = is_void_element;
}
}
ElementEnd { end: Empty, .. } => {
if node_stack.len() > 1 {
node_stack.pop();
}
last_was_void = false;
}
ElementEnd {
end: Close(_, close_value),
..
} => {
if last_was_void {
node_stack.pop();
last_was_void = false;
}
let is_void_element = VOID_ELEMENTS.contains(&close_value.as_str());
if is_void_element {
continue;
}
let close_value_str = close_value.as_str();
let mut found_idx = None;
for i in (1..node_stack.len()).rev() {
let node = unsafe { &*node_stack[i] };
if node.node_type.as_str() == close_value_str {
found_idx = Some(i);
break;
}
}
if let Some(idx) = found_idx {
node_stack.truncate(idx);
}
last_was_void = false;
}
Attribute { local, value, .. } => {
if let Some(&last_ptr) = node_stack.last() {
let last = unsafe { &mut *last_ptr };
last.attributes.push(azul_core::window::AzStringPair {
key: local.to_string().into(),
value: AzString::from(&*decode_xml_entities(value.as_str())),
});
}
}
Text { text } => {
if last_was_void {
node_stack.pop();
last_was_void = false;
}
let text_str = text.as_str();
if !text_str.is_empty() {
if let Some(¤t_parent_ptr) = node_stack.last() {
let current_parent = unsafe { &mut *current_parent_ptr };
let decoded_text = decode_xml_entities(text_str);
current_parent
.children
.push(XmlNodeChild::Text(AzString::from(&*decoded_text)));
}
}
}
_ => {}
}
}
if last_was_void {
node_stack.pop();
}
if node_stack.len() != 1 {
return Err(XmlError::UnclosedRootNode);
}
Ok(root_node.children.into())
}
#[cfg(feature = "xml")]
pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
Ok(Xml {
root: parse_xml_string(s)?.into(),
})
}
#[cfg(not(feature = "xml"))]
pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
Err(XmlError::NoParserAvailable)
}
#[cfg(feature = "xml")]
#[must_use] pub fn translate_roxmltree_expandedname(
e: roxmltree::ExpandedName<'_, '_>,
) -> XmlQualifiedName {
let ns: Option<AzString> = e.namespace().map(|e| e.to_string().into());
XmlQualifiedName {
local_name: e.name().to_string().into(),
namespace: ns.into(),
}
}
#[cfg(feature = "xml")]
fn translate_roxmltree_attribute(e: roxmltree::Attribute<'_, '_>) -> XmlQualifiedName {
XmlQualifiedName {
local_name: e.name().to_string().into(),
namespace: e.namespace().map(|e| e.to_string().into()).into(),
}
}
#[cfg(feature = "xml")]
fn translate_xmlparser_streamerror(e: xmlparser::StreamError) -> XmlStreamError {
match e {
xmlparser::StreamError::UnexpectedEndOfStream => XmlStreamError::UnexpectedEndOfStream,
xmlparser::StreamError::InvalidName => XmlStreamError::InvalidName,
xmlparser::StreamError::InvalidReference => XmlStreamError::InvalidReference,
xmlparser::StreamError::InvalidExternalID => XmlStreamError::InvalidExternalID,
xmlparser::StreamError::InvalidCommentData => XmlStreamError::InvalidCommentData,
xmlparser::StreamError::InvalidCommentEnd => XmlStreamError::InvalidCommentEnd,
xmlparser::StreamError::InvalidCharacterData => XmlStreamError::InvalidCharacterData,
xmlparser::StreamError::NonXmlChar(c, tp) => XmlStreamError::NonXmlChar(NonXmlCharError {
ch: c.into(),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::StreamError::InvalidChar(a, b, tp) => {
XmlStreamError::InvalidChar(InvalidCharError {
expected: a,
got: b,
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::StreamError::InvalidCharMultiple(a, b, tp) => {
XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
expected: a,
got: b.to_vec().into(),
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::StreamError::InvalidQuote(a, tp) => {
XmlStreamError::InvalidQuote(InvalidQuoteError {
got: a,
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::StreamError::InvalidSpace(a, tp) => {
XmlStreamError::InvalidSpace(InvalidSpaceError {
got: a,
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::StreamError::InvalidString(a, tp) => {
XmlStreamError::InvalidString(InvalidStringError {
got: a.to_string().into(),
pos: translate_xmlparser_textpos(tp),
})
}
}
}
#[cfg(feature = "xml")]
fn translate_xmlparser_error(e: xmlparser::Error) -> XmlParseError {
match e {
xmlparser::Error::InvalidDeclaration(se, tp) => {
XmlParseError::InvalidDeclaration(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::Error::InvalidComment(se, tp) => XmlParseError::InvalidComment(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidPI(se, tp) => XmlParseError::InvalidPI(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidDoctype(se, tp) => XmlParseError::InvalidDoctype(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidEntity(se, tp) => XmlParseError::InvalidEntity(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidElement(se, tp) => XmlParseError::InvalidElement(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidAttribute(se, tp) => {
XmlParseError::InvalidAttribute(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
})
}
xmlparser::Error::InvalidCdata(se, tp) => XmlParseError::InvalidCdata(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::InvalidCharData(se, tp) => XmlParseError::InvalidCharData(XmlTextError {
stream_error: translate_xmlparser_streamerror(se),
pos: translate_xmlparser_textpos(tp),
}),
xmlparser::Error::UnknownToken(tp) => {
XmlParseError::UnknownToken(translate_xmlparser_textpos(tp))
}
}
}
#[cfg(feature = "xml")]
#[must_use] pub fn translate_roxmltree_error(e: roxmltree::Error) -> XmlError {
match e {
roxmltree::Error::InvalidXmlPrefixUri(s) => {
XmlError::InvalidXmlPrefixUri(translate_roxml_textpos(s))
}
roxmltree::Error::UnexpectedXmlUri(s) => {
XmlError::UnexpectedXmlUri(translate_roxml_textpos(s))
}
roxmltree::Error::UnexpectedXmlnsUri(s) => {
XmlError::UnexpectedXmlnsUri(translate_roxml_textpos(s))
}
roxmltree::Error::InvalidElementNamePrefix(s) => {
XmlError::InvalidElementNamePrefix(translate_roxml_textpos(s))
}
roxmltree::Error::DuplicatedNamespace(s, tp) => {
XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
ns: s.into(),
pos: translate_roxml_textpos(tp),
})
}
roxmltree::Error::UnknownNamespace(s, tp) => {
XmlError::UnknownNamespace(UnknownNamespaceError {
ns: s.into(),
pos: translate_roxml_textpos(tp),
})
}
roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
expected: expected.into(),
actual: actual.into(),
pos: translate_roxml_textpos(pos),
})
}
roxmltree::Error::UnexpectedEntityCloseTag(s) => {
XmlError::UnexpectedEntityCloseTag(translate_roxml_textpos(s))
}
roxmltree::Error::UnknownEntityReference(s, tp) => {
XmlError::UnknownEntityReference(UnknownEntityReferenceError {
entity: s.into(),
pos: translate_roxml_textpos(tp),
})
}
roxmltree::Error::MalformedEntityReference(s) => {
XmlError::MalformedEntityReference(translate_roxml_textpos(s))
}
roxmltree::Error::EntityReferenceLoop(s) => {
XmlError::EntityReferenceLoop(translate_roxml_textpos(s))
}
roxmltree::Error::InvalidAttributeValue(s) => {
XmlError::InvalidAttributeValue(translate_roxml_textpos(s))
}
roxmltree::Error::DuplicatedAttribute(s, tp) => {
XmlError::DuplicatedAttribute(DuplicatedAttributeError {
attribute: s.into(),
pos: translate_roxml_textpos(tp),
})
}
roxmltree::Error::NoRootNode => XmlError::NoRootNode,
roxmltree::Error::DtdDetected => XmlError::DtdDetected,
roxmltree::Error::UnclosedRootNode => XmlError::UnclosedRootNode,
roxmltree::Error::UnexpectedDeclaration(tp) => {
XmlError::UnexpectedDeclaration(translate_roxml_textpos(tp))
}
roxmltree::Error::NodesLimitReached => XmlError::NodesLimitReached,
roxmltree::Error::AttributesLimitReached => XmlError::AttributesLimitReached,
roxmltree::Error::NamespacesLimitReached => XmlError::NamespacesLimitReached,
roxmltree::Error::InvalidName(tp) => XmlError::InvalidName(translate_roxml_textpos(tp)),
roxmltree::Error::NonXmlChar(_, tp) => XmlError::NonXmlChar(translate_roxml_textpos(tp)),
roxmltree::Error::InvalidChar(_, _, tp) => {
XmlError::InvalidChar(translate_roxml_textpos(tp))
}
roxmltree::Error::InvalidChar2(_, _, tp) => {
XmlError::InvalidChar2(translate_roxml_textpos(tp))
}
roxmltree::Error::InvalidString(_, tp) => {
XmlError::InvalidString(translate_roxml_textpos(tp))
}
roxmltree::Error::InvalidExternalID(tp) => {
XmlError::InvalidExternalID(translate_roxml_textpos(tp))
}
roxmltree::Error::InvalidComment(tp) => {
XmlError::InvalidComment(translate_roxml_textpos(tp))
}
roxmltree::Error::InvalidCharacterData(tp) => {
XmlError::InvalidCharacterData(translate_roxml_textpos(tp))
}
roxmltree::Error::UnknownToken(tp) => XmlError::UnknownToken(translate_roxml_textpos(tp)),
roxmltree::Error::UnexpectedEndOfStream => XmlError::UnexpectedEndOfStream,
roxmltree::Error::EntityResolver(tp, s) => {
XmlError::UnknownEntityReference(UnknownEntityReferenceError {
entity: s.into(),
pos: translate_roxml_textpos(tp),
})
}
}
}
#[cfg(feature = "xml")]
#[inline]
const fn translate_xmlparser_textpos(o: xmlparser::TextPos) -> XmlTextPos {
XmlTextPos {
row: o.row,
col: o.col,
}
}
#[cfg(feature = "xml")]
#[inline]
const fn translate_roxml_textpos(o: roxmltree::TextPos) -> XmlTextPos {
XmlTextPos {
row: o.row,
col: o.col,
}
}
#[cfg(feature = "xml")]
pub trait DomXmlExt {
fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom;
}
#[cfg(feature = "xml")]
impl DomXmlExt for Dom {
fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom {
let component_map = ComponentMap::with_builtin();
let dom_xml = domxml_from_str(xml.as_ref(), &component_map);
dom_xml.parsed_dom
}
}
#[cfg(test)]
mod autotest_generated {
use azul_core::dom::{FastDom, NodeData, NodeType, TabIndex};
use super::*;
#[cfg(feature = "xml")]
fn elements(children: &[XmlNodeChild]) -> Vec<&XmlNode> {
children
.iter()
.filter_map(XmlNodeChild::as_element)
.collect()
}
#[cfg(feature = "xml")]
fn texts(children: &[XmlNodeChild]) -> Vec<&str> {
children.iter().filter_map(XmlNodeChild::as_text).collect()
}
fn doc(body: &str) -> String {
format!("<html><body>{body}</body></html>")
}
fn nodes(dom: &FastDom) -> &[NodeData] {
dom.node_data.as_ref()
}
fn text_of(nd: &NodeData) -> Option<String> {
match nd.get_node_type() {
NodeType::Text(_) => nd.get_node_type().format(),
_ => None,
}
}
#[cfg(feature = "xml")]
fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
const GARBAGE: &[&str] = &[
"<<<<>>>>",
"!!!not xml at all!!!",
"<a b=c>",
"</>",
"</div>",
"<a></a",
"&&&&&&&&&&&&",
"]]>",
"<!--",
"<![CDATA[",
"<?",
"<!DOCTYPE",
"\u{0}\u{1}\u{2}",
"<a><<a><<<a>",
"= = = = = = = = = =",
];
#[test]
fn decode_xml_entities_borrows_when_there_is_no_ampersand() {
for s in ["", "hello", " ", "日本語 🙂", "<tag/>", "a;b;c;"] {
assert!(
matches!(decode_xml_entities(s), std::borrow::Cow::Borrowed(_)),
"{s:?} has no '&' and must take the zero-alloc path"
);
assert_eq!(&*decode_xml_entities(s), s);
}
}
#[test]
fn decode_xml_entities_decodes_the_five_named_entities_and_nbsp() {
assert_eq!(&*decode_xml_entities("<"), "<");
assert_eq!(&*decode_xml_entities(">"), ">");
assert_eq!(&*decode_xml_entities("&"), "&");
assert_eq!(&*decode_xml_entities("'"), "'");
assert_eq!(&*decode_xml_entities("""), "\"");
assert_eq!(&*decode_xml_entities(" "), "\u{00A0}");
assert_eq!(
&*decode_xml_entities("a<b>c&d"e'f"),
"a<b>c&d\"e'f"
);
}
#[test]
fn decode_xml_entities_decodes_numeric_references() {
assert_eq!(&*decode_xml_entities("<"), "<");
assert_eq!(&*decode_xml_entities("<"), "<");
assert_eq!(&*decode_xml_entities("<"), "<");
assert_eq!(&*decode_xml_entities("A"), "A");
assert_eq!(&*decode_xml_entities("�"), "\u{0}");
assert_eq!(&*decode_xml_entities(""), "\u{FFFF}");
assert_eq!(&*decode_xml_entities("𐀀"), "\u{10000}");
assert_eq!(&*decode_xml_entities(""), "\u{10FFFF}");
assert_eq!(&*decode_xml_entities(""), "\u{10FFFF}");
assert_eq!(&*decode_xml_entities("é"), "e\u{301}");
}
#[test]
fn decode_xml_entities_keeps_out_of_range_and_surrogate_code_points_verbatim() {
for s in [
"�", "�", "�", "�", "�", "�", "�", "�",
"&#x;",
"&#;",
"&#xZZ;",
"&#-1;",
] {
assert_eq!(
&*decode_xml_entities(s),
s,
"{s:?} is not a decodable reference and must be preserved byte-for-byte"
);
}
}
#[test]
fn decode_xml_entities_keeps_unterminated_and_unknown_entities_verbatim() {
for s in [
"&",
"&&",
"<",
"&#",
"&#x",
"&foo;",
"<", "&Amp;",
"& lt;",
"a & b",
"&;",
] {
assert_eq!(&*decode_xml_entities(s), s, "{s:?} must be preserved");
}
assert_eq!(&*decode_xml_entities("<&"), "<&");
}
#[test]
fn decode_xml_entities_does_not_double_decode() {
assert_eq!(&*decode_xml_entities("&lt;"), "<");
assert_eq!(&*decode_xml_entities("&amp;"), "&");
assert_eq!(&*decode_xml_entities("&#60;"), "<");
}
#[test]
fn decode_xml_entities_handles_pathological_input_without_panicking() {
let long_name = format!("&{};", "a".repeat(10_000));
assert_eq!(&*decode_xml_entities(&long_name), long_name);
let long_tail = format!("&{}", "x".repeat(1_000_000));
assert_eq!(decode_xml_entities(&long_tail).len(), long_tail.len());
for s in [
"&\u{1F600}\u{1F600};",
"&½;",
"&日本;",
"&#\u{1F600};",
"&e\u{301};",
"🙂&🙂",
] {
let out = decode_xml_entities(s);
assert!(
!out.is_empty(),
"{s:?} decoded to nothing (input was non-empty)"
);
}
let many = "<".repeat(50_000);
assert_eq!(decode_xml_entities(&many).chars().count(), 50_000);
}
#[test]
fn decode_xml_entities_slow_matches_the_fast_path() {
for s in [
"",
"plain",
"日本語 🙂",
"<",
"&lt;",
"😀",
"&unknown;",
"&",
] {
assert_eq!(
&*decode_xml_entities(s),
&*decode_xml_entities_slow(s),
"fast/slow path disagree on {s:?}"
);
}
for s in ["", "plain", "日本語 🙂", "a;b", "<>"] {
assert_eq!(&*decode_xml_entities_slow(s), s);
}
}
#[cfg(feature = "xml")]
#[test]
fn parse_entrypoints_do_not_panic_on_short_multibyte_input() {
const INPUT: &str = "😀😀😀";
assert!(INPUT.len() > 9 && !INPUT.is_char_boundary(9));
let a = std::panic::catch_unwind(|| parse_xml_string(INPUT).is_ok());
let b = std::panic::catch_unwind(|| parse_xml_to_fast_dom(INPUT).is_ok());
assert!(
a.is_ok(),
"parse_xml_string panicked on {INPUT:?}: the DOCTYPE sniff slices \
xml[..9] without an is_char_boundary check"
);
assert!(
b.is_ok(),
"parse_xml_to_fast_dom panicked on {INPUT:?}: same unchecked \
xml[..9] slice"
);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_accepts_empty_and_whitespace_only_input() {
for s in ["", " ", " ", "\t\n", "\r\n\r\n", "\u{FEFF}", "\u{FEFF} "] {
let parsed = parse_xml_string(s)
.unwrap_or_else(|e| panic!("{s:?} should parse to an empty tree, got {e}"));
assert!(parsed.is_empty(), "{s:?} produced {} roots", parsed.len());
}
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_parses_a_minimal_document() {
let parsed = parse_xml_string(&doc("<div>hi</div>")).expect("valid document");
let roots = elements(&parsed);
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].node_type.as_str(), "html");
let body = elements(roots[0].children.as_ref());
assert_eq!(body.len(), 1);
assert_eq!(body[0].node_type.as_str(), "body");
let div = elements(body[0].children.as_ref());
assert_eq!(div.len(), 1);
assert_eq!(div[0].node_type.as_str(), "div");
assert_eq!(texts(div[0].children.as_ref()), vec!["hi"]);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_rejects_unclosed_elements() {
assert!(matches!(
parse_xml_string("<div>"),
Err(XmlError::UnclosedRootNode)
));
assert!(matches!(
parse_xml_string("<html><body><div>"),
Err(XmlError::UnclosedRootNode)
));
assert!(
parse_xml_string("<html><body><div>text").is_err(),
"an unclosed element must not yield a partial 'valid' tree"
);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_rejects_truncated_declaration_and_doctype() {
assert!(matches!(
parse_xml_string("<?xml version=\"1.0\""),
Err(XmlError::MalformedHierarchy(_))
));
assert!(matches!(
parse_xml_string("<!DOCTYPE html PUBLIC \"x\""),
Err(XmlError::MalformedHierarchy(_))
));
for prefix in [
"<?xml version=\"1.0\"?>",
"<!DOCTYPE html>",
"<!doctype HTML>",
"<!-- leading comment -->",
"\u{FEFF}",
] {
let src = format!("{prefix}{}", doc("<div/>"));
let parsed = parse_xml_string(&src)
.unwrap_or_else(|e| panic!("{prefix:?} prefix should be stripped, got {e}"));
let roots = elements(&parsed);
assert_eq!(roots.len(), 1, "{prefix:?} -> {roots:?}");
assert_eq!(roots[0].node_type.as_str(), "html");
}
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_is_deterministic_on_garbage() {
for g in GARBAGE {
let a = parse_xml_string(g);
let b = parse_xml_string(g);
assert_eq!(a.is_ok(), b.is_ok(), "{g:?} parsed non-deterministically");
assert_eq!(a.ok(), b.ok(), "{g:?} produced two different trees");
}
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_trims_leading_and_trailing_whitespace() {
let padded = format!(" \t\n{}\n\t ", doc("<div/>"));
let a = parse_xml_string(&padded).expect("padded document");
let b = parse_xml_string(&doc("<div/>")).expect("bare document");
assert_eq!(a, b, "surrounding whitespace must not change the tree");
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_keeps_trailing_junk_as_text() {
let parsed = parse_xml_string(&format!("{};garbage", doc("<div/>"))).expect("lenient parse");
assert_eq!(elements(&parsed).len(), 1);
assert_eq!(texts(&parsed), vec![";garbage"]);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_round_trips_escaped_text() {
for raw in [
"a",
"<b>bold</b> & \"quotes\" 'apos'",
"&&&&",
" spaced ",
"日本語 🙂 combining e\u{301}",
"1 < 2 > 0 && true",
] {
let src = doc(&escape(raw));
let parsed = parse_xml_string(&src)
.unwrap_or_else(|e| panic!("{src:?} should parse, got {e}"));
let html = elements(&parsed);
let body = elements(html[0].children.as_ref());
assert_eq!(
texts(body[0].children.as_ref()),
vec![raw],
"escape -> parse must be the identity for {raw:?}"
);
}
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_decodes_attribute_entities() {
let parsed = parse_xml_string(&doc(
r#"<div t="&<>"'x" u="&nosuch;" v="😀"></div>"#,
))
.expect("valid document");
let html = elements(&parsed);
let body = elements(html[0].children.as_ref());
let div = elements(body[0].children.as_ref());
let attrs = &div[0].attributes;
assert_eq!(attrs.get_key("t").map(AzString::as_str), Some("&<>\"'x"));
assert_eq!(attrs.get_key("u").map(AzString::as_str), Some("&nosuch;"));
assert_eq!(attrs.get_key("v").map(AzString::as_str), Some("😀"));
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_tolerates_extra_and_mismatched_close_tags() {
let nested = doc("<div></span></div>");
let paragraphs = doc("<p>one<p>two");
for src in [
"<a></a></a>",
"<a></a></b>",
nested.as_str(),
paragraphs.as_str(),
"<br></br>",
"<br>",
"<br><br><br>",
] {
let a = parse_xml_string(src);
let b = parse_xml_string(src);
assert_eq!(a.is_ok(), b.is_ok(), "{src:?} is non-deterministic");
assert_eq!(a.ok(), b.ok(), "{src:?} produced two different trees");
}
let parsed = parse_xml_string("<br>").expect("bare void element");
assert_eq!(elements(&parsed).len(), 1);
assert_eq!(elements(&parsed)[0].node_type.as_str(), "br");
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_handles_deep_nesting_without_stack_overflow() {
const DEPTH: usize = 1_000;
let mut src = String::with_capacity(DEPTH * 12);
for _ in 0..DEPTH {
src.push_str("<a>");
}
for _ in 0..DEPTH {
src.push_str("</a>");
}
let parsed = parse_xml_string(&src).expect("balanced nesting is valid");
let mut depth = 0_usize;
{
let mut cursor: Vec<&XmlNode> = elements(&parsed);
while !cursor.is_empty() {
depth += 1;
let node: &XmlNode = cursor[0];
cursor = elements(node.children.as_ref());
}
}
assert_eq!(depth, DEPTH, "every nesting level must be preserved");
drop(parsed);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_handles_a_one_million_char_text_node() {
let payload = "x".repeat(1_000_000);
let parsed = parse_xml_string(&doc(&payload)).expect("long text is valid");
let html = elements(&parsed);
let body = elements(html[0].children.as_ref());
let t = texts(body[0].children.as_ref());
assert_eq!(t.len(), 1);
assert_eq!(t[0].len(), 1_000_000);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_string_handles_many_sibling_elements() {
const N: usize = 2_000;
let parsed = parse_xml_string(&doc(&"<i>x</i>".repeat(N))).expect("wide tree is valid");
let html = elements(&parsed);
let body = elements(html[0].children.as_ref());
assert_eq!(elements(body[0].children.as_ref()).len(), N);
}
#[cfg(feature = "xml")]
#[test]
fn parse_xml_agrees_with_parse_xml_string() {
let one = doc("<div>hi</div>");
let two = doc("<i/><i/>");
for src in ["", " ", one.as_str(), two.as_str()] {
let via_xml = parse_xml(src).expect("valid");
let via_string = parse_xml_string(src).expect("valid");
assert_eq!(
via_xml.root.as_ref(),
via_string.as_slice(),
"parse_xml must be a thin wrapper over parse_xml_string for {src:?}"
);
}
assert!(parse_xml("<div>").is_err());
}
#[cfg(not(feature = "xml"))]
#[test]
fn parse_xml_without_the_xml_feature_reports_no_parser() {
for s in ["", " ", "<div/>", "garbage"] {
assert!(matches!(parse_xml(s), Err(XmlError::NoParserAvailable)));
}
}
#[test]
fn parse_xml_to_fast_dom_accepts_empty_and_whitespace_only_input() {
for s in ["", " ", " ", "\t\n", "\u{FEFF}", "\u{FEFF} \n "] {
let dom = parse_xml_to_fast_dom(s)
.unwrap_or_else(|e| panic!("{s:?} should yield an empty arena, got {e}"));
assert!(nodes(&dom).is_empty(), "{s:?} produced {} nodes", nodes(&dom).len());
assert_eq!(dom.node_hierarchy.as_ref().len(), nodes(&dom).len());
}
}
#[test]
fn parse_xml_to_fast_dom_builds_the_expected_arena() {
let dom = parse_xml_to_fast_dom(&doc("<div>hi</div>")).expect("valid document");
let n = nodes(&dom);
assert_eq!(n.len(), 4, "html + body + div + text");
assert_eq!(
dom.node_hierarchy.as_ref().len(),
n.len(),
"hierarchy and node_data arenas must stay parallel"
);
assert!(matches!(n[0].get_node_type(), NodeType::Html));
assert!(matches!(n[1].get_node_type(), NodeType::Body));
assert!(matches!(n[2].get_node_type(), NodeType::Div));
assert_eq!(text_of(&n[3]).as_deref(), Some("hi"));
}
#[test]
fn parse_xml_to_fast_dom_lowercases_tag_names() {
let dom = parse_xml_to_fast_dom("<HTML><BODY><DiV/></BODY></HTML>").expect("valid");
let n = nodes(&dom);
assert_eq!(n.len(), 3);
assert!(matches!(n[0].get_node_type(), NodeType::Html));
assert!(matches!(n[1].get_node_type(), NodeType::Body));
assert!(matches!(n[2].get_node_type(), NodeType::Div));
}
#[test]
fn parse_xml_to_fast_dom_skips_head_but_collects_style_css() {
let src = "<html><head><title>T</title>\
<style>div { width: 10px; }</style></head>\
<body>x</body></html>";
let (dom, css) = parse_xml_to_fast_dom_with_css(src).expect("valid document");
let n = nodes(&dom);
assert_eq!(n.len(), 3, "html + body + text; <head> subtree is dropped");
assert!(
!n.iter()
.any(|nd| matches!(nd.get_node_type(), NodeType::Head | NodeType::Title)),
"no <head>/<title> node may reach the arena"
);
assert_eq!(text_of(&n[2]).as_deref(), Some("x"));
assert_eq!(css.len(), 1, "the <style> body must still be collected");
assert!(!css[0].rules.as_ref().is_empty(), "the CSS must have parsed");
}
#[test]
fn parse_xml_to_fast_dom_splits_ids_and_classes_on_whitespace() {
let dom = parse_xml_to_fast_dom(&doc(r#"<div id="a b" class="c d
e"></div>"#))
.expect("valid document");
let div = &nodes(&dom)[2];
assert!(div.has_id("a") && div.has_id("b"));
assert!(div.has_class("c") && div.has_class("d") && div.has_class("e"));
assert!(!div.has_id("a b"), "the raw joined value must not survive");
assert_eq!(div.get_ids_and_classes().as_ref().len(), 5);
}
fn tab_index_with(attrs: &str) -> Option<TabIndex> {
let dom = parse_xml_to_fast_dom(&doc(&format!("<div {attrs}></div>")))
.unwrap_or_else(|e| panic!("{attrs:?} should parse, got {e}"));
nodes(&dom)[2].get_tab_index()
}
#[test]
fn parse_xml_to_fast_dom_maps_tabindex_boundaries() {
assert_eq!(tab_index_with(r#"tabindex="0""#), Some(TabIndex::Auto));
assert_eq!(tab_index_with(r#"tabindex="-0""#), Some(TabIndex::Auto));
assert_eq!(
tab_index_with(r#"tabindex="1""#),
Some(TabIndex::OverrideInParent(1))
);
assert_eq!(
tab_index_with(r#"tabindex="+3""#),
Some(TabIndex::OverrideInParent(3)),
"isize::from_str accepts a leading '+'"
);
assert_eq!(
tab_index_with(r#"tabindex="-1""#),
Some(TabIndex::NoKeyboardFocus)
);
assert_eq!(
tab_index_with(r#"tabindex="-9223372036854775808""#),
Some(TabIndex::NoKeyboardFocus),
"i64::MIN is still just 'negative'"
);
const MAX_EXACT: u32 = (1 << 28) - 1;
assert_eq!(
tab_index_with(&format!(r#"tabindex="{MAX_EXACT}""#)),
Some(TabIndex::OverrideInParent(MAX_EXACT))
);
assert_eq!(
tab_index_with(r#"tabindex="268435456""#),
Some(TabIndex::OverrideInParent(0)),
"1 << 28 truncates to 0"
);
assert_eq!(
tab_index_with(r#"tabindex="9223372036854775807""#),
Some(TabIndex::OverrideInParent(MAX_EXACT)),
"i64::MAX -> u32::MAX -> 28-bit mask"
);
}
#[test]
fn parse_xml_to_fast_dom_ignores_unparseable_tabindex() {
let baseline = tab_index_with("");
for junk in [
r#"tabindex="""#,
r#"tabindex="NaN""#,
r#"tabindex="inf""#,
r#"tabindex="-inf""#,
r#"tabindex="1.0""#,
r#"tabindex="1e5""#,
r#"tabindex=" 3 ""#,
r#"tabindex="0x10""#,
r#"tabindex="99999999999999999999999999""#,
r#"tabindex="-99999999999999999999999999""#,
r#"tabindex="🙂""#,
] {
assert_eq!(
tab_index_with(junk),
baseline,
"{junk} must leave the tab index untouched"
);
}
}
#[test]
fn parse_xml_to_fast_dom_parses_bool_attributes_case_sensitively() {
assert_eq!(
tab_index_with(r#"focusable="true""#),
Some(TabIndex::Auto)
);
assert_eq!(
tab_index_with(r#"focusable="false""#),
Some(TabIndex::NoKeyboardFocus)
);
for junk in [r#"focusable="TRUE""#, r#"focusable="1""#, r#"focusable="yes""#] {
assert_eq!(
tab_index_with(junk),
tab_index_with(""),
"{junk} is not a bool literal and must be ignored"
);
}
let editable = |v: &str| {
let dom = parse_xml_to_fast_dom(&doc(&format!(r#"<div contenteditable="{v}"></div>"#)))
.expect("valid");
nodes(&dom)[2].is_contenteditable()
};
assert!(editable("true"));
assert!(!editable("false"));
assert!(!editable("TRUE"));
assert!(!editable(""));
assert!(!editable("1"));
}
#[test]
fn parse_xml_to_fast_dom_survives_malformed_style_attributes() {
let big = "a:b;".repeat(2_000);
for style in [
"",
";;;;",
"::::",
":",
"width",
"width:",
":10px",
"a:b:c",
";:;:;:",
"width:10px",
"width:10px;;;height:;;",
"width:not-a-length",
"🙂:🙂",
big.as_str(),
] {
let dom = parse_xml_to_fast_dom(&doc(&format!(r#"<div style="{style}"></div>"#)))
.unwrap_or_else(|e| panic!("style={style:?} should parse, got {e}"));
assert_eq!(
nodes(&dom).len(),
3,
"style={style:?} must not change the node count"
);
}
}
#[test]
fn parse_xml_to_fast_dom_survives_unbalanced_tags() {
let dom = parse_xml_to_fast_dom("<html><head><title>").expect("lenient parse");
assert_eq!(nodes(&dom).len(), 1, "only <html> survives");
assert!(matches!(nodes(&dom)[0].get_node_type(), NodeType::Html));
for src in [
"</div>",
"</div></div></div>",
"<a></a></a>",
"<html><body></body></body></html>",
"<html><head><head><head>",
"<html><body><div></span></div></body></html>",
] {
let a = parse_xml_to_fast_dom(src);
let b = parse_xml_to_fast_dom(src);
assert_eq!(a.is_ok(), b.is_ok(), "{src:?} is non-deterministic");
if let (Ok(a), Ok(b)) = (&a, &b) {
assert_eq!(nodes(a).len(), nodes(b).len(), "{src:?} node count drifted");
}
}
}
#[test]
fn parse_xml_to_fast_dom_is_deterministic_on_garbage() {
for g in GARBAGE {
let a = parse_xml_to_fast_dom(g);
let b = parse_xml_to_fast_dom(g);
assert_eq!(a.is_ok(), b.is_ok(), "{g:?} parsed non-deterministically");
if let (Ok(a), Ok(b)) = (&a, &b) {
assert_eq!(nodes(a).len(), nodes(b).len(), "{g:?} node count drifted");
}
}
}
#[test]
fn parse_xml_to_fast_dom_handles_ten_thousand_nested_elements() {
const DEPTH: usize = 10_000;
let mut src = String::with_capacity(DEPTH * 12 + 32);
src.push_str("<html><body>");
for _ in 0..DEPTH {
src.push_str("<div>");
}
for _ in 0..DEPTH {
src.push_str("</div>");
}
src.push_str("</body></html>");
let dom = parse_xml_to_fast_dom(&src).expect("balanced nesting is valid");
assert_eq!(nodes(&dom).len(), DEPTH + 2);
}
#[test]
fn parse_xml_to_fast_dom_handles_a_one_million_char_document() {
let payload = "x".repeat(1_000_000);
let dom = parse_xml_to_fast_dom(&doc(&payload)).expect("long text is valid");
let n = nodes(&dom);
assert_eq!(n.len(), 3, "html + body + one text node");
assert_eq!(text_of(&n[2]).map(|s| s.len()), Some(1_000_000));
}
#[test]
fn parse_xml_to_fast_dom_strips_bom_declaration_doctype_and_comments() {
let expected = nodes(&parse_xml_to_fast_dom(&doc("<div/>")).expect("baseline")).len();
for prefix in [
"\u{FEFF}",
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<!DOCTYPE html>",
"<!doctype HTML>",
"<!DoCtYpE html SYSTEM \"about:legacy-compat\">",
"<!-- leading comment -->",
] {
let src = format!("{prefix}{}", doc("<div/>"));
let dom = parse_xml_to_fast_dom(&src)
.unwrap_or_else(|e| panic!("{prefix:?} should be stripped, got {e}"));
assert_eq!(nodes(&dom).len(), expected, "{prefix:?} changed the arena");
}
}
#[test]
fn parse_xml_to_fast_dom_preserves_unicode_text() {
for payload in [
"日本語",
"🙂🙂🙂🙂",
"e\u{301}\u{302}\u{303}",
"\u{200B}\u{FEFF}mid-string BOM",
"ï·º",
] {
let dom = parse_xml_to_fast_dom(&doc(payload))
.unwrap_or_else(|e| panic!("{payload:?} should parse, got {e}"));
let n = nodes(&dom);
assert_eq!(n.len(), 3, "{payload:?}");
assert_eq!(text_of(&n[2]).as_deref(), Some(payload));
}
}
#[test]
fn parse_xml_to_fast_dom_treats_numeric_looking_documents_as_text() {
for payload in [
"0",
"-0",
"9223372036854775807",
"-9223372036854775808",
"18446744073709551616",
"1e309",
"-1e-309",
"NaN",
"inf",
"-inf",
] {
let dom = parse_xml_to_fast_dom(&doc(payload))
.unwrap_or_else(|e| panic!("{payload:?} should parse, got {e}"));
assert_eq!(text_of(&nodes(&dom)[2]).as_deref(), Some(payload));
}
}
#[test]
fn parse_xml_to_styled_dom_accepts_empty_and_whitespace_only_input() {
for s in ["", " ", "\t\n", "\u{FEFF}"] {
let styled = parse_xml_to_styled_dom(s)
.unwrap_or_else(|e| panic!("{s:?} should cascade cleanly, got {e}"));
assert!(styled.node_data.as_ref().is_empty(), "{s:?}");
}
}
#[test]
fn parse_xml_to_styled_dom_keeps_the_fast_dom_node_count() {
for src in [
doc("<div>hi</div>"),
doc("<div><span>a</span><span>b</span></div>"),
"<html><head><style>div { width: 10px; }</style></head><body><div/></body></html>"
.to_string(),
] {
let fast = parse_xml_to_fast_dom(&src).expect("fast path");
let styled = parse_xml_to_styled_dom(&src).expect("styled path");
assert_eq!(
styled.node_data.as_ref().len(),
nodes(&fast).len(),
"the cascade must not add or drop nodes for {src:?}"
);
assert_eq!(
styled.node_hierarchy.as_ref().len(),
styled.node_data.as_ref().len()
);
}
}
#[test]
fn parse_xml_to_styled_dom_is_deterministic_on_garbage() {
for g in GARBAGE {
let a = parse_xml_to_styled_dom(g);
let b = parse_xml_to_styled_dom(g);
assert_eq!(a.is_ok(), b.is_ok(), "{g:?} cascaded non-deterministically");
}
}
#[test]
fn dom_from_parsed_xml_reports_errors_instead_of_panicking() {
for root in [
Vec::new(),
vec![XmlNodeChild::Text("bare text".into())],
vec![XmlNodeChild::Element(XmlNode::create("div"))],
vec![XmlNodeChild::Element(XmlNode::create("html"))],
] {
let dom = dom_from_parsed_xml(Xml { root: root.into() });
assert!(
matches!(dom.root.get_node_type(), NodeType::Body),
"the error Dom is rendered as a <body> with a label"
);
assert_eq!(dom.children.as_ref().len(), 1);
}
}
#[test]
fn dom_from_parsed_xml_builds_a_dom_for_a_minimal_document() {
let body = XmlNode::create("body")
.with_children(vec![XmlNodeChild::Element(XmlNode::create("div"))]);
let html = XmlNode::create("html").with_children(vec![XmlNodeChild::Element(body)]);
let dom = dom_from_parsed_xml(Xml {
root: vec![XmlNodeChild::Element(html)].into(),
});
assert!(matches!(dom.root.get_node_type(), NodeType::Html));
assert_eq!(dom.children.as_ref().len(), 1, "the <body> subtree");
}
#[test]
fn dom_from_parsed_xml_caps_recursion_on_deeply_nested_input() {
const DEPTH: usize = 550;
let mut node = XmlNode::create("div");
for _ in 0..DEPTH {
node = XmlNode::create("div").with_children(vec![XmlNodeChild::Element(node)]);
}
let body = XmlNode::create("body").with_children(vec![XmlNodeChild::Element(node)]);
let html = XmlNode::create("html").with_children(vec![XmlNodeChild::Element(body)]);
let dom = dom_from_parsed_xml(Xml {
root: vec![XmlNodeChild::Element(html)].into(),
});
assert!(matches!(dom.root.get_node_type(), NodeType::Html));
}
#[cfg(feature = "xml")]
#[test]
fn domxml_from_str_never_fails() {
let map = ComponentMap::with_builtin();
let mut cases: Vec<String> = GARBAGE.iter().map(|s| (*s).to_string()).collect();
cases.push(String::new());
cases.push(" ".to_string());
cases.push("<svg".to_string());
cases.push("<?xml".to_string());
cases.push(doc("<div>hi</div>"));
for src in cases {
let dom_xml = domxml_from_str(&src, &map);
assert!(
!dom_xml.parsed_dom.node_data.as_ref().is_empty(),
"{src:?} produced an empty StyledDom; errors must render as a label"
);
}
}
#[cfg(all(feature = "std", feature = "xml"))]
#[test]
fn domxml_from_file_renders_io_errors_as_a_dom() {
let map = ComponentMap::with_builtin();
for path in [
"/nonexistent-azul-autotest-dir/definitely-not-here.xml",
"",
"/",
"/proc/self/nonexistent-🙂",
] {
let dom_xml = domxml_from_file(path, &map);
assert!(
!dom_xml.parsed_dom.node_data.as_ref().is_empty(),
"{path:?} must render the io::Error as a label, not fail"
);
}
}
#[cfg(feature = "xml")]
#[test]
fn dom_xml_ext_matches_domxml_from_str() {
let map = ComponentMap::with_builtin();
let valid = doc("<div>hi</div>");
for src in ["", "<svg", valid.as_str()] {
let via_ext = <Dom as DomXmlExt>::from_xml_string(src);
let via_fn = domxml_from_str(src, &map).parsed_dom;
assert_eq!(
via_ext.node_data.as_ref().len(),
via_fn.node_data.as_ref().len(),
"the extension trait must be a pure delegation for {src:?}"
);
}
}
#[test]
fn peak_rss_bytes_never_panics_and_never_goes_backwards() {
let a = peak_rss_bytes();
let _ballast = "x".repeat(4 * 1024 * 1024);
let b = peak_rss_bytes();
#[cfg(all(unix, feature = "probe"))]
assert!(
b >= a,
"ru_maxrss is a high-water mark and must never decrease ({a} -> {b})"
);
#[cfg(not(all(unix, feature = "probe")))]
assert_eq!(
(a, b),
(0, 0),
"without the probe feature the stub must be a constant 0"
);
}
#[cfg(feature = "xml")]
#[test]
fn translate_textpos_round_trips_boundary_values() {
for (row, col) in [
(0, 0),
(1, 1),
(0, u32::MAX),
(u32::MAX, 0),
(u32::MAX, u32::MAX),
] {
let expected = XmlTextPos { row, col };
assert_eq!(
translate_xmlparser_textpos(xmlparser::TextPos::new(row, col)),
expected
);
assert_eq!(
translate_roxml_textpos(roxmltree::TextPos::new(row, col)),
expected
);
}
}
#[cfg(feature = "xml")]
#[test]
fn translate_roxmltree_expandedname_preserves_name_and_namespace() {
let plain: roxmltree::ExpandedName<'_, '_> = "rect".into();
let out = translate_roxmltree_expandedname(plain);
assert_eq!(out.local_name.as_str(), "rect");
assert!(out.namespace.as_ref().is_none());
let ns: roxmltree::ExpandedName<'_, '_> = ("http://www.w3.org/2000/svg", "rect").into();
let out = translate_roxmltree_expandedname(ns);
assert_eq!(out.local_name.as_str(), "rect");
assert_eq!(
out.namespace.as_ref().map(AzString::as_str),
Some("http://www.w3.org/2000/svg")
);
for name in ["", " ", "日本語-🙂", "a:b"] {
let e: roxmltree::ExpandedName<'_, '_> = name.into();
assert_eq!(translate_roxmltree_expandedname(e).local_name.as_str(), name);
}
let empty_ns: roxmltree::ExpandedName<'_, '_> = ("", "x").into();
assert_eq!(
translate_roxmltree_expandedname(empty_ns)
.namespace
.as_ref()
.map(AzString::as_str),
Some(""),
"an empty namespace URI is Some(\"\"), not None"
);
}
#[cfg(feature = "xml")]
#[test]
fn translate_roxmltree_attribute_preserves_name_and_namespace() {
let rdoc = roxmltree::Document::parse(r#"<e xmlns:x="urn:x" x:a="1" b="2"/>"#)
.expect("valid XML");
let attrs: Vec<XmlQualifiedName> = rdoc
.root_element()
.attributes()
.map(translate_roxmltree_attribute)
.collect();
assert_eq!(attrs.len(), 2, "xmlns declarations are not attributes");
let a = attrs
.iter()
.find(|q| q.local_name.as_str() == "a")
.expect("x:a");
assert_eq!(a.namespace.as_ref().map(AzString::as_str), Some("urn:x"));
let b = attrs
.iter()
.find(|q| q.local_name.as_str() == "b")
.expect("b");
assert!(
b.namespace.as_ref().is_none(),
"an unprefixed attribute has no namespace"
);
}
#[cfg(feature = "xml")]
#[test]
fn translate_xmlparser_streamerror_maps_every_variant() {
use xmlparser::StreamError as Se;
let p = xmlparser::TextPos::new(3, 7);
let x = XmlTextPos { row: 3, col: 7 };
assert_eq!(
translate_xmlparser_streamerror(Se::UnexpectedEndOfStream),
XmlStreamError::UnexpectedEndOfStream
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidName),
XmlStreamError::InvalidName
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidReference),
XmlStreamError::InvalidReference
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidExternalID),
XmlStreamError::InvalidExternalID
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidCommentData),
XmlStreamError::InvalidCommentData
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidCommentEnd),
XmlStreamError::InvalidCommentEnd
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidCharacterData),
XmlStreamError::InvalidCharacterData
);
assert_eq!(
translate_xmlparser_streamerror(Se::NonXmlChar('\u{1F600}', p)),
XmlStreamError::NonXmlChar(NonXmlCharError {
ch: 0x1F600,
pos: x
})
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidQuote(b'`', p)),
XmlStreamError::InvalidQuote(InvalidQuoteError { got: b'`', pos: x })
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidSpace(b'\t', p)),
XmlStreamError::InvalidSpace(InvalidSpaceError { got: b'\t', pos: x })
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidString("?>", p)),
XmlStreamError::InvalidString(InvalidStringError {
got: "?>".into(),
pos: x
})
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidChar(b'a', b'b', p)),
XmlStreamError::InvalidChar(InvalidCharError {
expected: b'a',
got: b'b',
pos: x
})
);
assert_eq!(
translate_xmlparser_streamerror(Se::InvalidCharMultiple(b'a', &b"xy"[..], p)),
XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
expected: b'a',
got: vec![b'x', b'y'].into(),
pos: x
})
);
}
#[cfg(feature = "xml")]
#[test]
fn translate_xmlparser_error_maps_every_variant() {
use xmlparser::{Error as Xe, StreamError as Se};
let p = xmlparser::TextPos::new(9, 4);
let x = XmlTextPos { row: 9, col: 4 };
let te = XmlTextError {
stream_error: XmlStreamError::InvalidName,
pos: x,
};
assert_eq!(
translate_xmlparser_error(Xe::InvalidDeclaration(Se::InvalidName, p)),
XmlParseError::InvalidDeclaration(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidComment(Se::InvalidName, p)),
XmlParseError::InvalidComment(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidPI(Se::InvalidName, p)),
XmlParseError::InvalidPI(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidDoctype(Se::InvalidName, p)),
XmlParseError::InvalidDoctype(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidEntity(Se::InvalidName, p)),
XmlParseError::InvalidEntity(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidElement(Se::InvalidName, p)),
XmlParseError::InvalidElement(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidAttribute(Se::InvalidName, p)),
XmlParseError::InvalidAttribute(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidCdata(Se::InvalidName, p)),
XmlParseError::InvalidCdata(te.clone())
);
assert_eq!(
translate_xmlparser_error(Xe::InvalidCharData(Se::InvalidName, p)),
XmlParseError::InvalidCharData(te)
);
assert_eq!(
translate_xmlparser_error(Xe::UnknownToken(p)),
XmlParseError::UnknownToken(x)
);
}
#[cfg(feature = "xml")]
#[test]
fn translate_roxmltree_error_maps_every_variant() {
use roxmltree::Error as Re;
let p = roxmltree::TextPos::new(2, 5);
let x = XmlTextPos { row: 2, col: 5 };
assert_eq!(
translate_roxmltree_error(Re::InvalidXmlPrefixUri(p)),
XmlError::InvalidXmlPrefixUri(x)
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedXmlUri(p)),
XmlError::UnexpectedXmlUri(x)
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedXmlnsUri(p)),
XmlError::UnexpectedXmlnsUri(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidElementNamePrefix(p)),
XmlError::InvalidElementNamePrefix(x)
);
assert_eq!(
translate_roxmltree_error(Re::DuplicatedNamespace(String::from("ns"), p)),
XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
ns: "ns".into(),
pos: x
})
);
assert_eq!(
translate_roxmltree_error(Re::UnknownNamespace(String::from("ns"), p)),
XmlError::UnknownNamespace(UnknownNamespaceError {
ns: "ns".into(),
pos: x
})
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedCloseTag(
String::from("a"),
String::from("b"),
p
)),
XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
expected: "a".into(),
actual: "b".into(),
pos: x
})
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedEntityCloseTag(p)),
XmlError::UnexpectedEntityCloseTag(x)
);
assert_eq!(
translate_roxmltree_error(Re::UnknownEntityReference(String::from("e"), p)),
XmlError::UnknownEntityReference(UnknownEntityReferenceError {
entity: "e".into(),
pos: x
})
);
assert_eq!(
translate_roxmltree_error(Re::MalformedEntityReference(p)),
XmlError::MalformedEntityReference(x)
);
assert_eq!(
translate_roxmltree_error(Re::EntityReferenceLoop(p)),
XmlError::EntityReferenceLoop(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidAttributeValue(p)),
XmlError::InvalidAttributeValue(x)
);
assert_eq!(
translate_roxmltree_error(Re::DuplicatedAttribute(String::from("a"), p)),
XmlError::DuplicatedAttribute(DuplicatedAttributeError {
attribute: "a".into(),
pos: x
})
);
assert_eq!(
translate_roxmltree_error(Re::NoRootNode),
XmlError::NoRootNode
);
assert_eq!(
translate_roxmltree_error(Re::DtdDetected),
XmlError::DtdDetected
);
assert_eq!(
translate_roxmltree_error(Re::UnclosedRootNode),
XmlError::UnclosedRootNode
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedDeclaration(p)),
XmlError::UnexpectedDeclaration(x)
);
assert_eq!(
translate_roxmltree_error(Re::NodesLimitReached),
XmlError::NodesLimitReached
);
assert_eq!(
translate_roxmltree_error(Re::AttributesLimitReached),
XmlError::AttributesLimitReached
);
assert_eq!(
translate_roxmltree_error(Re::NamespacesLimitReached),
XmlError::NamespacesLimitReached
);
assert_eq!(
translate_roxmltree_error(Re::InvalidName(p)),
XmlError::InvalidName(x)
);
assert_eq!(
translate_roxmltree_error(Re::NonXmlChar('\u{0}', p)),
XmlError::NonXmlChar(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidChar(b'a', b'b', p)),
XmlError::InvalidChar(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidChar2("ab", b'c', p)),
XmlError::InvalidChar2(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidString("s", p)),
XmlError::InvalidString(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidExternalID(p)),
XmlError::InvalidExternalID(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidComment(p)),
XmlError::InvalidComment(x)
);
assert_eq!(
translate_roxmltree_error(Re::InvalidCharacterData(p)),
XmlError::InvalidCharacterData(x)
);
assert_eq!(
translate_roxmltree_error(Re::UnknownToken(p)),
XmlError::UnknownToken(x)
);
assert_eq!(
translate_roxmltree_error(Re::UnexpectedEndOfStream),
XmlError::UnexpectedEndOfStream
);
assert_eq!(
translate_roxmltree_error(Re::EntityResolver(p, String::from("e"))),
XmlError::UnknownEntityReference(UnknownEntityReferenceError {
entity: "e".into(),
pos: x
})
);
}
}