Files
moli/moli-parser/src/xml.rs
T
ldm0 bd7055dc02 fix(runtime): reconcile WPT changes with native state on main
Restore native History cursors from sparse serialized positions, preserve
shared Window history across isolated realms, and keep entry identity and
structured state through document.open and cross-realm settlement.

Move event subclass and precommit state access onto shared native backing.
Respect published iframe layout snapshots and align history, rendering,
and rejection tests with their owning task boundaries. Remove duplicate
upstream tests and obsolete helpers from the main rebase.

Validation: cargo fmt --all; workspace/all-targets/all-features Clippy with
-D warnings; full Nextest (20,319 passed, 16 skipped); fresh CLI HTTP/CDP
fetch resource timing fixture (36/36). Build and tests used 32-way parallelism.
2026-10-11 18:06:43 +08:00

1360 lines
46 KiB
Rust

use std::{
borrow::Cow,
cell::{Cell, RefCell},
collections::HashMap,
ops::{Deref, DerefMut},
rc::Rc,
};
use url::Url;
use xml5ever::{
Attribute as XmlAttribute, ExpandedName as XmlExpandedName, QualName as XmlQualName,
driver::{XmlParseOpts, parse_document as parse_xml_document},
interface::{ElementFlags as XmlElementFlags, QuirksMode as XmlQuirksMode},
tendril::{StrTendril as XmlStrTendril, TendrilSink as XmlTendrilSink},
tree_builder::{NodeOrText as XmlNodeOrText, TreeSink as XmlTreeSinkBase, XmlTreeSink},
};
use xmlparser::{
ElementEnd as XmlElementEnd, Token as XmlTokenizerToken, Tokenizer as XmlTokenizer,
};
use super::{
html_chunks, xml_preprocess::prepare_xml_for_xml5ever,
xml_tree_viewer::transform_document_to_xml_tree_view,
};
use moli_dom::native::{
Attribute as NativeAttribute, DomHost, NativeDom, NativeNodeId, Node, ParserConstruction,
};
#[derive(Debug, Clone, Default)]
pub struct XmlParser;
struct XmlPrepassError {
detail: String,
discard_partial_tree: bool,
}
impl XmlPrepassError {
fn tree(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
discard_partial_tree: false,
}
}
fn before_document_element(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
discard_partial_tree: true,
}
}
fn namespace(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
discard_partial_tree: true,
}
}
}
#[derive(Debug, Clone)]
pub(super) struct XmlParseHandle {
pub(super) node_id: NativeNodeId,
pub(super) element_name: Option<Rc<XmlQualName>>,
}
struct XmlDocumentSink<'a> {
target: RefCell<XmlLiveTreeSinkTarget<'a>>,
quirks_mode: Cell<XmlQuirksMode>,
}
enum XmlDomHost<'a> {
Owned(Box<DomHost>),
Borrowed(&'a mut DomHost),
}
impl Deref for XmlDomHost<'_> {
type Target = DomHost;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(dom_host) => dom_host.as_ref(),
Self::Borrowed(dom_host) => dom_host,
}
}
}
impl DerefMut for XmlDomHost<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Owned(dom_host) => dom_host.as_mut(),
Self::Borrowed(dom_host) => dom_host,
}
}
}
struct XmlLiveTreeSinkTarget<'a> {
construction: ParserConstruction,
dom_host: XmlDomHost<'a>,
document_handle: NativeNodeId,
}
impl XmlParser {
pub fn parse(&self, final_url: Url, xml: String) -> NativeDom {
self.parse_with_presentation(final_url, xml, false)
}
/// Parses a top-level XML navigation and applies Chromium's source-tree
/// presentation when the document has no associated style information.
/// DOMParser and child-document callers must use the raw parser entry point.
pub fn parse_top_level_document(&self, final_url: Url, xml: String) -> NativeDom {
self.parse_with_presentation(final_url, xml, true)
}
fn parse_with_presentation(
&self,
final_url: Url,
xml: String,
present_unstyled_xml: bool,
) -> NativeDom {
let xml = prepare_xml_for_xml5ever(&xml);
let prepass_error = xml_element_stack_error(&xml);
let sink = XmlDocumentSink::new(XmlLiveTreeSinkTarget::new_owned(final_url));
let mut parser = parse_xml_document(sink, XmlParseOpts::default());
for chunk in html_chunks(&xml) {
parser.process(XmlStrTendril::from(chunk));
}
let mut document = parser.finish().finish_document();
// xml5ever recovers from some mismatched element stacks. DOMParser must
// expose an XML parsererror document instead of that recovered tree.
if let Some(error) = prepass_error {
document.push_parse_error(error.detail);
if error.discard_partial_tree {
let mut host = DomHost::from_dom(document);
let document_handle = host.document_handle();
for child in host.child_handles(document_handle).collect::<Vec<_>>() {
let _ = host.remove_child(document_handle, child);
}
document = host.snapshot_document();
}
}
if present_unstyled_xml {
document = transform_document_to_xml_tree_view(document);
}
document
}
/// Parses an inert XML tree directly into an empty XML Document that
/// already belongs to `dom_host`.
///
/// The mutable borrow is retained by the tree sink for exactly this
/// synchronous parser call. Script elements remain inert; executable
/// top-level and child Documents must use `XmlDocumentStream` instead.
pub fn parse_inert_tree_into_document(
&self,
dom_host: &mut DomHost,
document_handle: NativeNodeId,
xml: &str,
) -> Option<()> {
let target = XmlLiveTreeSinkTarget::new_borrowed(dom_host, document_handle)?;
let sink = XmlDocumentSink::new(target);
let mut parser = parse_xml_document(sink, XmlParseOpts::default());
let xml = prepare_xml_for_xml5ever(xml);
for chunk in html_chunks(&xml) {
parser.process(XmlStrTendril::from(chunk));
}
parser.finish().finish_live_tree();
Some(())
}
}
fn xml_element_stack_error(xml: &str) -> Option<XmlPrepassError> {
let mut pending_start_name = None;
let mut pending_used_prefixes = Vec::new();
let mut pending_declared_prefixes = Vec::new();
let mut open_elements = Vec::new();
let mut active_prefixes = HashMap::from([("xml".to_owned(), 1usize)]);
let mut saw_document_element = false;
for token in XmlTokenizer::from(xml) {
match token {
Ok(XmlTokenizerToken::ElementStart { prefix, local, .. }) => {
saw_document_element = true;
let prefix = prefix.as_str();
if !prefix.is_empty() {
pending_used_prefixes.push(prefix.to_owned());
}
pending_start_name = Some(qualified_xml_name(prefix, local.as_str()));
}
Ok(XmlTokenizerToken::Attribute {
prefix,
local,
value,
..
}) => {
let prefix = prefix.as_str();
if prefix == "xmlns" {
if !value.as_str().is_empty() {
pending_declared_prefixes.push(local.as_str().to_owned());
}
} else if !prefix.is_empty() {
pending_used_prefixes.push(prefix.to_owned());
}
}
Ok(XmlTokenizerToken::ElementEnd { end, .. }) => match end {
XmlElementEnd::Open => {
let Some(name) = pending_start_name.take() else {
return Some(XmlPrepassError::tree("XML start tag is missing a name"));
};
for prefix in &pending_declared_prefixes {
*active_prefixes.entry(prefix.clone()).or_default() += 1;
}
if let Some(prefix) = pending_used_prefixes
.iter()
.find(|prefix| !active_prefixes.contains_key(*prefix))
{
return Some(XmlPrepassError::namespace(format!(
"XML namespace prefix `{prefix}` is not declared"
)));
}
pending_used_prefixes.clear();
open_elements.push((name, std::mem::take(&mut pending_declared_prefixes)));
}
XmlElementEnd::Empty => {
if pending_start_name.take().is_none() {
return Some(XmlPrepassError::tree(
"XML empty-element tag is missing a name",
));
}
for prefix in &pending_declared_prefixes {
*active_prefixes.entry(prefix.clone()).or_default() += 1;
}
if let Some(prefix) = pending_used_prefixes
.iter()
.find(|prefix| !active_prefixes.contains_key(*prefix))
{
return Some(XmlPrepassError::namespace(format!(
"XML namespace prefix `{prefix}` is not declared"
)));
}
pending_used_prefixes.clear();
for prefix in pending_declared_prefixes.drain(..) {
decrement_xml_prefix_count(&mut active_prefixes, &prefix);
}
}
XmlElementEnd::Close(prefix, local) => {
let closing_name = qualified_xml_name(prefix.as_str(), local.as_str());
let Some((open_name, declared_prefixes)) = open_elements.pop() else {
return Some(XmlPrepassError::tree(format!(
"XML end tag </{closing_name}> has no matching start tag"
)));
};
if open_name != closing_name {
return Some(XmlPrepassError::tree(format!(
"XML end tag </{closing_name}> does not match <{open_name}>"
)));
}
for prefix in declared_prefixes {
decrement_xml_prefix_count(&mut active_prefixes, &prefix);
}
}
},
Ok(_) => {}
Err(error) => {
let detail = error.to_string();
return Some(if saw_document_element {
XmlPrepassError::tree(detail)
} else {
// xml5ever can recover a malformed prologue or doctype and
// then expose nodes parsed after the error. DOMParser error
// documents must not retain that recovered document tree.
XmlPrepassError::before_document_element(detail)
});
}
}
}
if let Some(name) = pending_start_name {
Some(XmlPrepassError::tree(format!(
"XML start tag <{name}> is incomplete"
)))
} else {
open_elements
.last()
.map(|(name, _)| XmlPrepassError::tree(format!("XML element <{name}> is not closed")))
}
}
fn decrement_xml_prefix_count(active_prefixes: &mut HashMap<String, usize>, prefix: &str) {
let Some(count) = active_prefixes.get_mut(prefix) else {
return;
};
*count -= 1;
if *count == 0 {
active_prefixes.remove(prefix);
}
}
fn qualified_xml_name(prefix: &str, local_name: &str) -> String {
if prefix.is_empty() {
local_name.to_owned()
} else {
format!("{prefix}:{local_name}")
}
}
impl XmlParseHandle {
pub(super) fn new(node_id: NativeNodeId, element_name: Option<Rc<XmlQualName>>) -> Self {
Self {
node_id,
element_name,
}
}
}
impl PartialEq for XmlParseHandle {
fn eq(&self, other: &Self) -> bool {
self.node_id == other.node_id
}
}
impl Eq for XmlParseHandle {}
impl<'a> XmlDocumentSink<'a> {
fn new(target: XmlLiveTreeSinkTarget<'a>) -> Self {
Self {
target: RefCell::new(target),
quirks_mode: Cell::new(XmlQuirksMode::NoQuirks),
}
}
}
impl XmlLiveTreeSinkTarget<'static> {
fn new_owned(final_url: Url) -> Self {
let dom_host = DomHost::from_dom(NativeDom::new_xml(final_url));
let document_handle = dom_host.document_handle();
Self {
construction: ParserConstruction::default(),
dom_host: XmlDomHost::Owned(Box::new(dom_host)),
document_handle,
}
}
fn finish_document(self) -> NativeDom {
let XmlDomHost::Owned(dom_host) = self.dom_host else {
unreachable!("owned XML parsing must finish with an owned DOM host")
};
dom_host.snapshot_document()
}
}
impl<'a> XmlLiveTreeSinkTarget<'a> {
fn new_borrowed(dom_host: &'a mut DomHost, document_handle: NativeNodeId) -> Option<Self> {
let is_empty_xml_document = dom_host
.node(document_handle)
.and_then(Node::as_document)
.is_some_and(|document| !document.is_html_document())
&& dom_host.child_handles(document_handle).next().is_none();
if !is_empty_xml_document {
return None;
}
Some(Self {
construction: ParserConstruction::default(),
dom_host: XmlDomHost::Borrowed(dom_host),
document_handle,
})
}
fn finish_live_tree(self) {}
fn document_handle(&self) -> XmlParseHandle {
XmlParseHandle::new(self.document_handle, None)
}
fn create_element(&mut self, name: XmlQualName, attrs: Vec<XmlAttribute>) -> XmlParseHandle {
let element_name = Rc::new(name.clone());
let attributes = attrs
.into_iter()
.map(|attribute| {
NativeAttribute::new(
attribute.name.local.to_string(),
attribute.name.ns.to_string(),
attribute
.name
.prefix
.as_ref()
.map(|prefix| prefix.to_string()),
attribute.value.to_string(),
)
})
.collect::<Vec<_>>();
let node_id = self.construction.create_element(
&mut self.dom_host,
self.document_handle,
name.local.to_string(),
name.ns.to_string(),
name.prefix.as_ref().map(|prefix| prefix.to_string()),
);
self.dom_host
.add_attrs_if_missing_for_parser(node_id, attributes);
XmlParseHandle::new(node_id, Some(element_name))
}
fn create_comment(&mut self, text: String) -> XmlParseHandle {
XmlParseHandle::new(
self.dom_host
.create_comment_for_document(self.document_handle, &text),
None,
)
}
fn create_cdata(&mut self, text: String) -> XmlParseHandle {
XmlParseHandle::new(
self.dom_host
.create_cdata_section_for_document(self.document_handle, &text),
None,
)
}
fn create_processing_instruction(&mut self, target: String, data: String) -> XmlParseHandle {
XmlParseHandle::new(
self.dom_host.create_processing_instruction_for_document(
self.document_handle,
&target,
&data,
),
None,
)
}
fn append_text(&mut self, parent_id: NativeNodeId, text: String) {
if parent_id == self.document_handle && text.trim().is_empty() {
return;
}
self.insert_text_node(parent_id, None, text);
}
fn insert_text_node(
&mut self,
parent_id: NativeNodeId,
reference_child: Option<NativeNodeId>,
text: String,
) {
if parent_id == self.document_handle && text.trim().is_empty() {
return;
}
if text.is_empty() || self.dom_host.node(parent_id).is_none() {
return;
}
if let Some(reference_child) = reference_child {
if self
.dom_host
.node(reference_child)
.and_then(Node::as_text)
.is_some()
{
if let Some(text_node) = self
.dom_host
.node_mut(reference_child)
.and_then(|node| node.data_mut().as_text_mut())
{
let mut merged = text;
merged.push_str(text_node.data());
text_node.set_data(merged);
}
return;
}
if let Some(previous) = self
.dom_host
.node(reference_child)
.and_then(Node::prev_sibling)
&& self
.dom_host
.node(previous)
.and_then(Node::as_text)
.is_some()
{
if let Some(text_node) = self
.dom_host
.node_mut(previous)
.and_then(|node| node.data_mut().as_text_mut())
{
let mut merged = text_node.data().to_owned();
merged.push_str(&text);
text_node.set_data(merged);
}
return;
}
} else if let Some(last_child) = self.dom_host.node(parent_id).and_then(Node::last_child)
&& self
.dom_host
.node(last_child)
.and_then(Node::as_text)
.is_some()
{
if let Some(text_node) = self
.dom_host
.node_mut(last_child)
.and_then(|node| node.data_mut().as_text_mut())
{
let mut merged = text_node.data().to_owned();
merged.push_str(&text);
text_node.set_data(merged);
}
return;
}
let text_node = self
.dom_host
.create_text_node_for_document(self.document_handle, &text);
if let Some(reference_child) = reference_child {
let Some(parent) = self
.dom_host
.node(reference_child)
.and_then(Node::parent_node)
else {
return;
};
let _ = self
.dom_host
.insert_before(parent, text_node, Some(reference_child));
} else {
let _ = self.dom_host.append_child(parent_id, text_node);
}
}
fn append(&mut self, parent_id: NativeNodeId, child: XmlNodeOrText<XmlParseHandle>) {
let parent_id = self.template_contents_id(parent_id).unwrap_or(parent_id);
match child {
XmlNodeOrText::AppendNode(handle) => {
if self.is_xml_declaration_processing_instruction(handle.node_id) {
return;
}
let _ = self.dom_host.append_child(parent_id, handle.node_id);
}
XmlNodeOrText::AppendText(text) => self.append_text(parent_id, text.to_string()),
}
}
fn append_before_sibling(
&mut self,
sibling_id: NativeNodeId,
child: XmlNodeOrText<XmlParseHandle>,
) {
let Some(parent_id) = self.dom_host.node(sibling_id).and_then(Node::parent_node) else {
return;
};
match child {
XmlNodeOrText::AppendNode(handle) => {
if self.is_xml_declaration_processing_instruction(handle.node_id) {
return;
}
let _ = self
.dom_host
.insert_before(parent_id, handle.node_id, Some(sibling_id));
}
XmlNodeOrText::AppendText(text) => {
self.insert_text_node(parent_id, Some(sibling_id), text.to_string());
}
}
}
fn append_based_on_parent_node(
&mut self,
element_id: NativeNodeId,
prev_element_id: NativeNodeId,
child: XmlNodeOrText<XmlParseHandle>,
) {
if self
.dom_host
.node(element_id)
.and_then(Node::parent_node)
.is_some()
{
self.append_before_sibling(element_id, child);
} else {
self.append(prev_element_id, child);
}
}
fn append_doctype(&mut self, name: String, public_id: String, system_id: String) {
let doctype = self.dom_host.create_document_type_for_document(
self.document_handle,
&name,
&public_id,
&system_id,
);
let _ = self.dom_host.append_child(self.document_handle, doctype);
}
fn is_xml_declaration_processing_instruction(&self, node_id: NativeNodeId) -> bool {
self.dom_host
.node(node_id)
.and_then(Node::as_processing_instruction)
.is_some_and(|pi| pi.target().eq_ignore_ascii_case("xml"))
}
fn template_contents_handle(&self, node_id: NativeNodeId) -> Option<XmlParseHandle> {
self.dom_host
.parser_template_contents_handle(node_id)
.map(|handle| XmlParseHandle::new(handle, None))
}
fn template_contents_id(&self, node_id: NativeNodeId) -> Option<NativeNodeId> {
self.dom_host.parser_template_contents_handle(node_id)
}
fn add_attrs_if_missing(&mut self, node_id: NativeNodeId, attrs: Vec<XmlAttribute>) {
let Some(node) = self.dom_host.node_mut(node_id) else {
return;
};
let Some(element) = node.data_mut().as_element_mut() else {
return;
};
for attribute in attrs {
let already_exists = element.attributes().iter().any(|existing| {
existing.namespace() == attribute.name.ns.as_ref()
&& existing.local_name() == attribute.name.local.as_ref()
&& existing.prefix()
== attribute.name.prefix.as_ref().map(|prefix| prefix.as_ref())
});
if !already_exists {
element.set_attribute(
attribute.name.local.to_string(),
attribute.name.ns.to_string(),
attribute.name.prefix.map(|prefix| prefix.to_string()),
attribute.value.to_string(),
);
}
}
}
fn remove_from_parent(&mut self, node_id: NativeNodeId) {
let Some(parent_id) = self.dom_host.node(node_id).and_then(Node::parent_node) else {
return;
};
let _ = self.dom_host.remove_child(parent_id, node_id);
}
fn reparent_children(&mut self, node_id: NativeNodeId, new_parent_id: NativeNodeId) {
let child_ids = self.dom_host.child_handles(node_id).collect::<Vec<_>>();
for child_id in child_ids {
let _ = self.dom_host.append_child(new_parent_id, child_id);
}
}
}
impl<'host> XmlTreeSinkBase for XmlDocumentSink<'host> {
type Handle = XmlParseHandle;
type Output = XmlLiveTreeSinkTarget<'host>;
type ElemName<'a>
= XmlExpandedName<'a>
where
Self: 'a;
fn finish(self) -> Self::Output {
let target = self.target.into_inner();
target.construction.finish();
target
}
fn parse_error(&self, err: Cow<'static, str>) {
self.target
.borrow_mut()
.dom_host
.push_parse_error(err.into_owned());
}
fn get_document(&self) -> Self::Handle {
self.target.borrow().document_handle()
}
fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
target
.element_name
.as_deref()
.expect("xml5ever requested the name of a non-element node")
.expanded()
}
fn create_element(
&self,
name: XmlQualName,
attrs: Vec<XmlAttribute>,
_flags: XmlElementFlags,
) -> Self::Handle {
self.target.borrow_mut().create_element(name, attrs)
}
fn create_comment(&self, text: XmlStrTendril) -> Self::Handle {
self.target.borrow_mut().create_comment(text.to_string())
}
fn create_pi(&self, target: XmlStrTendril, data: XmlStrTendril) -> Self::Handle {
self.target
.borrow_mut()
.create_processing_instruction(target.to_string(), data.to_string())
}
fn append(&self, parent: &Self::Handle, child: XmlNodeOrText<Self::Handle>) {
self.target.borrow_mut().append(parent.node_id, child);
}
fn append_before_sibling(&self, sibling: &Self::Handle, child: XmlNodeOrText<Self::Handle>) {
self.target
.borrow_mut()
.append_before_sibling(sibling.node_id, child);
}
fn append_based_on_parent_node(
&self,
element: &Self::Handle,
prev_element: &Self::Handle,
child: XmlNodeOrText<Self::Handle>,
) {
self.target.borrow_mut().append_based_on_parent_node(
element.node_id,
prev_element.node_id,
child,
);
}
fn append_doctype_to_document(
&self,
name: XmlStrTendril,
public_id: XmlStrTendril,
system_id: XmlStrTendril,
) {
self.target.borrow_mut().append_doctype(
name.to_string(),
public_id.to_string(),
system_id.to_string(),
);
}
fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
self.target
.borrow()
.template_contents_handle(target.node_id)
.unwrap_or_else(|| target.clone())
}
fn same_node(&self, left: &Self::Handle, right: &Self::Handle) -> bool {
left == right
}
fn pop(&self, node: &Self::Handle) {
let target = &mut *self.target.borrow_mut();
let _ = target
.construction
.finish_children(&mut target.dom_host, node.node_id);
}
fn set_quirks_mode(&self, mode: XmlQuirksMode) {
self.quirks_mode.set(mode);
}
fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec<XmlAttribute>) {
self.target
.borrow_mut()
.add_attrs_if_missing(target.node_id, attrs);
}
fn remove_from_parent(&self, target: &Self::Handle) {
self.target.borrow_mut().remove_from_parent(target.node_id);
}
fn reparent_children(&self, node: &Self::Handle, new_parent: &Self::Handle) {
self.target
.borrow_mut()
.reparent_children(node.node_id, new_parent.node_id);
}
}
impl XmlTreeSink for XmlDocumentSink<'_> {
fn create_cdata(&self, text: XmlStrTendril) -> Self::Handle {
self.target.borrow_mut().create_cdata(text.to_string())
}
}
#[cfg(test)]
mod tests {
use super::XmlParser;
use moli_dom::native::{DomHost, NativeDom, NativeNodeId, Node, NodeType};
use url::Url;
#[test]
fn xml_numeric_references_follow_xml_chars_without_html_replacements() {
for unit in [
0x9, 0xa, 0xd, 0x20, 0x7f, 0x80, 0x85, 0x91, 0x9f, 0xd7ff, 0xe000, 0xfdd0, 0xfdef,
0xfffd, 0x10000, 0x1fffe, 0x1ffff, 0x10ffff,
] {
let expected = char::from_u32(unit).unwrap().to_string();
for reference in [format!("&#{unit};"), format!("&#x{unit:X};")] {
for quote in ['\'', '"'] {
let source = format!(
"<root xmlns:p='urn:test' p:v={quote}{reference}{quote}>{reference}</root>"
);
let dom = XmlParser.parse(
Url::parse("https://xml.test/character-reference.xml").unwrap(),
source.clone(),
);
assert!(
dom.parse_errors().is_empty(),
"{source}: {:?}",
dom.parse_errors()
);
let root = dom.document_element_node_id().unwrap();
let element = dom.node(root).and_then(Node::as_element).unwrap();
let attribute = element
.attributes()
.iter()
.find(|attribute| attribute.namespace() == "urn:test")
.unwrap();
assert_eq!(attribute.value(), expected, "{source}");
assert_eq!(
dom.text_content(root).as_deref(),
Some(expected.as_str()),
"{source}"
);
}
}
}
}
#[test]
fn xml_numeric_references_reject_values_outside_xml_char() {
for value in [
"0",
"1",
"8",
"11",
"12",
"14",
"31",
"55296",
"57343",
"65534",
"65535",
"1114112",
"999999999999999999999999999999999999",
] {
for source in [
format!("<root v='&#{value};'/>"),
format!("<root>&#{value};</root>"),
] {
let dom = XmlParser.parse(
Url::parse("https://xml.test/invalid-character-reference.xml").unwrap(),
source.clone(),
);
assert!(!dom.parse_errors().is_empty(), "{source}");
}
}
}
#[test]
fn xml_style_children_finish_at_close() {
for namespace in ["http://www.w3.org/1999/xhtml", "http://www.w3.org/2000/svg"] {
let document = XmlParser.parse(
Url::parse("https://example.test/style.xml").unwrap(),
format!("<style xmlns='{namespace}'>p {{ color: blue; }}</style>"),
);
let root = document.document_element_node_id().unwrap();
assert!(
!document
.node(root)
.and_then(Node::as_element)
.unwrap()
.style_children_parsing()
);
}
}
#[test]
fn xml_parser_records_unclosed_and_mismatched_elements() {
for (index, source) in [
"<root>",
"<root><child></root>",
"<root></other>",
"</root>",
]
.into_iter()
.enumerate()
{
let dom = XmlParser.parse(
Url::parse(&format!("https://example.test/invalid-{index}.xml")).unwrap(),
source.to_owned(),
);
assert!(
!dom.parse_errors().is_empty(),
"source `{source}` should be rejected"
);
}
for (index, source) in ["<root/>", "<root><child/></root>"].into_iter().enumerate() {
let dom = XmlParser.parse(
Url::parse(&format!("https://example.test/valid-{index}.xml")).unwrap(),
source.to_owned(),
);
assert!(
dom.parse_errors().is_empty(),
"source `{source}`: {:?}",
dom.parse_errors()
);
}
let declared = XmlParser.parse(
Url::parse("https://example.test/declared-prefix.xml").unwrap(),
"<x:root xmlns:x='urn:test' x:value='ok'/>".to_owned(),
);
assert!(declared.parse_errors().is_empty());
let undeclared = XmlParser.parse(
Url::parse("https://example.test/undeclared-prefix.xml").unwrap(),
"<root x:value='invalid'/>".to_owned(),
);
assert!(!undeclared.parse_errors().is_empty());
assert!(
undeclared
.child_ids(undeclared.document_node_id())
.all(|child| !undeclared.node(child).is_some_and(Node::is_element))
);
}
#[test]
fn xml_parser_discards_recovered_tree_after_doctype_error() {
let url = Url::parse("https://example.test/doctype.xml").unwrap();
let invalid = XmlParser.parse(
url.clone(),
concat!(
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\">",
"<html><div id=\"test\"/></html>"
)
.to_owned(),
);
assert!(!invalid.parse_errors().is_empty());
assert!(
invalid
.child_ids(invalid.document_node_id())
.all(|child| !invalid.node(child).is_some_and(Node::is_element))
);
for system_id in ["", "x"] {
let valid = XmlParser.parse(
url.clone(),
format!(
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"{system_id}\"><html/>"
),
);
assert!(
valid.parse_errors().is_empty(),
"system ID `{system_id}` should be accepted: {:?}",
valid.parse_errors()
);
}
}
#[test]
fn xml_parser_preserves_namespace_declarations_as_dom_attributes() {
let dom = XmlParser.parse(
Url::parse("https://example.test/catalog.xml").unwrap(),
concat!(
"<catalog data-before='1' xmlns='urn:catalog&amp;items' ",
"data-middle='2' xmlns:m='urn:meta' data-after='3'>",
"<m:item xmlns='urn:nested'/>",
"</catalog>"
)
.to_owned(),
);
let root = document_element(&dom);
let root_attributes = dom
.node(root)
.and_then(Node::as_element)
.expect("catalog element")
.attributes();
assert_eq!(
root_attributes
.iter()
.map(|attribute| attribute.name())
.collect::<Vec<_>>(),
[
"data-before",
"xmlns",
"data-middle",
"xmlns:m",
"data-after"
]
);
assert_eq!(
root_attributes[1].namespace(),
"http://www.w3.org/2000/xmlns/"
);
assert_eq!(root_attributes[1].prefix(), None);
assert_eq!(root_attributes[1].local_name(), "xmlns");
assert_eq!(root_attributes[1].value(), "urn:catalog&items");
assert_eq!(
root_attributes[3].namespace(),
"http://www.w3.org/2000/xmlns/"
);
assert_eq!(root_attributes[3].prefix(), Some("xmlns"));
assert_eq!(root_attributes[3].local_name(), "m");
assert_eq!(root_attributes[3].value(), "urn:meta");
let child = dom
.child_ids(root)
.find(|node_id| dom.node(*node_id).is_some_and(Node::is_element))
.expect("nested item element");
let child_attributes = dom
.node(child)
.and_then(Node::as_element)
.expect("nested item")
.attributes();
assert_eq!(child_attributes.len(), 1);
assert_eq!(child_attributes[0].name(), "xmlns");
assert_eq!(child_attributes[0].value(), "urn:nested");
}
#[test]
fn xml_parser_finishes_styles_in_owned_and_borrowed_documents() {
let url = Url::parse("https://example.test/style.xml").unwrap();
for namespace in ["http://www.w3.org/1999/xhtml", "http://www.w3.org/2000/svg"] {
for ending in [
"/>",
"><![CDATA[body { color: red; }]]></style>",
">body { color: red; }",
] {
let source = format!("<style xmlns='{namespace}'{ending}");
let parsed = XmlParser.parse(url.clone(), source.clone());
let style = document_element(&parsed);
assert!(
!parsed
.node(style)
.and_then(Node::as_element)
.unwrap()
.style_children_parsing(),
"owned XML parse must finish style children: {source}"
);
let mut host = DomHost::from_dom(NativeDom::new_html(url.clone()));
let document = host.create_detached_xml_document();
XmlParser
.parse_inert_tree_into_document(&mut host, document, &source)
.unwrap();
let style = host.child_handles(document).next().unwrap();
assert!(
!host.is_style_element_parsing_children(style),
"borrowed XML parse must finish style children: {source}"
);
}
}
}
#[test]
fn xml_parser_populates_an_existing_xml_document_with_exact_node_owners() {
let parser = XmlParser;
let mut host = DomHost::from_dom(NativeDom::new_html(
Url::parse("https://parent.example.test/").unwrap(),
));
let parent_document = host.document_handle();
let child_url = Url::parse("https://child.example.test/document.xml").unwrap();
let child_document = host.create_detached_xml_document_with_url(child_url.clone());
parser
.parse_inert_tree_into_document(
&mut host,
child_document,
concat!(
"<?before data?>",
"<!DOCTYPE root>",
"<root xmlns:h='http://www.w3.org/1999/xhtml'>",
"<child attr='value'>text<![CDATA[cdata]]><!--comment--></child>",
"<h:script />",
"</root>"
),
)
.expect("empty XML document accepts a direct inert parse");
assert_eq!(host.child_handles(parent_document).count(), 0);
let mut descendants = Vec::new();
collect_descendants(&host, child_document, &mut descendants);
let script_handle = descendants
.iter()
.copied()
.find(|node_id| {
host.node(*node_id)
.is_some_and(|node| node.is_html_element_named("script"))
})
.expect("XHTML script element");
assert_eq!(
host.node(script_handle).and_then(Node::owner_document),
Some(child_document)
);
assert_eq!(
host.node(child_document)
.and_then(Node::as_document)
.map(|document| document.url()),
Some(&child_url)
);
assert!(
descendants
.iter()
.all(|node_id| host.node(*node_id).and_then(Node::owner_document)
== Some(child_document))
);
for expected in [
NodeType::ProcessingInstruction,
NodeType::DocumentType,
NodeType::Element,
NodeType::Text,
NodeType::CDataSection,
NodeType::Comment,
] {
assert!(descendants.iter().any(|node_id| {
host.node(*node_id)
.is_some_and(|node| node.node_type() == expected)
}));
}
}
#[test]
fn xml_parser_rejects_html_and_nonempty_document_targets() {
let parser = XmlParser;
let mut host = DomHost::from_dom(NativeDom::new_html(
Url::parse("https://example.test/").unwrap(),
));
let html_document = host.document_handle();
assert!(
parser
.parse_inert_tree_into_document(&mut host, html_document, "<root />")
.is_none()
);
assert_eq!(host.child_handles(html_document).count(), 0);
let xml_document = host.create_detached_xml_document();
assert!(
parser
.parse_inert_tree_into_document(&mut host, xml_document, "<root />")
.is_some()
);
let children_before_retry = host.child_handles(xml_document).collect::<Vec<_>>();
assert!(
parser
.parse_inert_tree_into_document(&mut host, xml_document, "<replacement />")
.is_none()
);
assert_eq!(
host.child_handles(xml_document).collect::<Vec<_>>(),
children_before_retry
);
}
#[test]
fn xml_parser_existing_document_html_template_uses_the_target_document_url() {
let parser = XmlParser;
let mut host = DomHost::from_dom(NativeDom::new_html(
Url::parse("https://parent.example.test/").unwrap(),
));
let child_url = Url::parse("https://child.example.test/document.xml").unwrap();
let child_document = host.create_detached_xml_document_with_url(child_url.clone());
parser
.parse_inert_tree_into_document(
&mut host,
child_document,
concat!(
"<root>",
"<template xmlns='http://www.w3.org/1999/xhtml'><span /></template>",
"</root>"
),
)
.expect("empty XML document accepts a direct parse");
let root = host
.child_handles(child_document)
.find(|node_id| host.node(*node_id).is_some_and(Node::is_element))
.expect("document element");
let template = host
.child_handles(root)
.find(|node_id| {
host.node(*node_id)
.is_some_and(|node| node.is_html_element_named("template"))
})
.expect("HTML template element");
let contents = host
.node(template)
.and_then(Node::as_element)
.and_then(|element| element.template_contents())
.expect("template contents");
let inert_document = host
.node(contents)
.and_then(Node::owner_document)
.expect("template contents owner document");
assert_ne!(inert_document, host.document_handle());
assert_ne!(inert_document, child_document);
assert_eq!(
host.node(inert_document)
.and_then(Node::as_document)
.map(|document| document.url()),
Some(&child_url)
);
}
fn collect_descendants(
host: &DomHost,
parent: NativeNodeId,
descendants: &mut Vec<NativeNodeId>,
) {
for child in host.child_handles(parent).collect::<Vec<_>>() {
descendants.push(child);
collect_descendants(host, child, descendants);
}
}
#[test]
fn xml_parser_preserves_cdata_section_nodes() {
let parser = XmlParser;
let dom = parser.parse(
Url::parse("https://example.test/xml").unwrap(),
"<foo>CDATA section: <![CDATA[ < > & ]]>.</foo>".to_owned(),
);
let document = dom.document_node_id();
let root = dom
.child_ids(document)
.find(|node_id| dom.node(*node_id).is_some_and(|node| node.is_element()))
.expect("document element");
let children = dom.child_ids(root).collect::<Vec<_>>();
assert_eq!(children.len(), 3);
assert_eq!(
dom.node(children[0]).map(|node| node.node_type()),
Some(NodeType::Text)
);
assert_eq!(
dom.node(children[1]).map(|node| node.node_type()),
Some(NodeType::CDataSection)
);
assert_eq!(
dom.node(children[1])
.and_then(|node| node.as_cdata_section())
.map(|cdata| cdata.data()),
Some(" < > & ")
);
assert_eq!(
dom.node(children[2]).map(|node| node.node_type()),
Some(NodeType::Text)
);
}
#[test]
fn xml_parser_puts_only_html_template_children_in_template_contents() {
let parser = XmlParser;
let html_dom = parser.parse(
Url::parse("https://example.test/html-template.xml").unwrap(),
"<template xmlns='http://www.w3.org/1999/xhtml'><test/></template>".to_owned(),
);
let html_template = document_element(&html_dom);
let html_contents = html_dom
.node(html_template)
.and_then(Node::as_element)
.and_then(|element| element.template_contents())
.expect("HTML template contents");
assert_eq!(html_dom.child_ids(html_template).count(), 0);
assert_eq!(
html_dom
.child_ids(html_contents)
.next()
.and_then(|child| html_dom.node(child))
.and_then(Node::as_element)
.map(|element| element.local_name()),
Some("test")
);
for (url, xml) in [
(
"https://example.test/no-namespace-template.xml",
"<template><test/></template>",
),
(
"https://example.test/svg-template.xml",
"<template xmlns='http://www.w3.org/2000/svg'><test/></template>",
),
] {
let dom = parser.parse(Url::parse(url).unwrap(), xml.to_owned());
let template = document_element(&dom);
assert!(
dom.node(template)
.and_then(Node::as_element)
.and_then(|element| element.template_contents())
.is_none()
);
assert_eq!(
dom.child_ids(template)
.next()
.and_then(|child| dom.node(child))
.and_then(Node::as_element)
.map(|element| element.local_name()),
Some("test")
);
}
}
#[test]
fn xml_parser_preserves_cdata_inside_html_template_contents() {
let parser = XmlParser;
let dom = parser.parse(
Url::parse("https://example.test/html-template-cdata.xml").unwrap(),
concat!(
"<template xmlns='http://www.w3.org/1999/xhtml'>",
"<![CDATA[top-level]]>",
"<test><![CDATA[nested]]></test>",
"</template>"
)
.to_owned(),
);
let template = document_element(&dom);
let contents = dom
.node(template)
.and_then(Node::as_element)
.and_then(|element| element.template_contents())
.expect("HTML template contents");
let content_children = dom.child_ids(contents).collect::<Vec<_>>();
assert_eq!(content_children.len(), 2);
assert_eq!(
dom.node(content_children[0])
.and_then(Node::as_cdata_section)
.map(|cdata| cdata.data()),
Some("top-level")
);
assert_eq!(
dom.node(content_children[1])
.and_then(Node::as_element)
.map(|element| element.local_name()),
Some("test")
);
assert_eq!(
dom.child_ids(content_children[1])
.next()
.and_then(|child| dom.node(child))
.and_then(Node::as_cdata_section)
.map(|cdata| cdata.data()),
Some("nested")
);
}
fn document_element(dom: &moli_dom::native::NativeDom) -> moli_dom::native::NativeNodeId {
dom.child_ids(dom.document_node_id())
.find(|node_id| dom.node(*node_id).is_some_and(|node| node.is_element()))
.expect("document element")
}
}