Files
moli/moli-parser/src/xml.rs
T
ldm0 7f08c25f08 fix(dom): preserve template document identity and adoption lifecycles
Share a lazy inert owner across each document's template contents and keep
HTML/XML semantics, interface identity, and template-owner state separate.
Cloning preserves interfaces while creating independent owner state.

Separate portable Document metadata from arena-local relationships. Use
one metadata-copy boundary for Document cloning and main-document
replacement, clearing template-owner and CSS-target handles while keeping
whole-DOM snapshots and copy-on-write storage relationships intact. Metadata
copies also discard base-element href/target caches while retaining about
base URLs and explicit overrides; only DOM node cloning freezes computed
base URLs. Detached Document URL readers use native metadata without JS
URL/documentURI/baseURI shadow state, including selector and bridge readers.

Apply the DOM-specific document cloning rules separately: freeze the source
computed base URL as an override, separately retain the inherited about
base URL, and rebuild base-element caches from the cloned tree. Preserve
main-document replacement metadata semantics.

Establish exact HTML template identity and associated fragment allocation
in one native entry shared by DOM and parser creation. Clone actual native
contents rather than guessing from names, preserving foreign and mixed-case
elements through serialization, deep cloning, import and adoption. Keep HTML
createElement normalization at its binding boundary; namespace-aware creation
and wrapper selection preserve the exact local name, including registry paths.

Plan per-node adoption targets across ordinary children, shadow roots and
template contents. Retarget custom-element registries and callbacks with
the actual destination documents, and clear cross-document shadow sheets
while retaining sheets within a document and its own template owner.
Avoid preparing unused inert style sources.

Cover cross-arena ID collisions, metadata preservation, snapshot isolation,
nested templates, deep adoption, iframe/detached documents, cloned
interfaces, retained sheet arrays, registry associations and URL behavior.
2026-10-29 02:54:08 +08:00

1217 lines
41 KiB
Rust

use std::{
borrow::Cow,
cell::{Cell, RefCell},
ops::{ControlFlow, 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 super::{html_chunks, 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;
#[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 sink = XmlDocumentSink::new(XmlLiveTreeSinkTarget::new_owned(final_url));
if sink.note_lexical_errors(&xml).is_break() {
return sink.finish().finish_document();
}
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();
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);
if sink.note_lexical_errors(xml).is_break() {
sink.finish().finish_live_tree();
return Some(());
}
let mut parser = parse_xml_document(sink, XmlParseOpts::default());
for chunk in html_chunks(xml) {
parser.process(XmlStrTendril::from(chunk));
}
parser.finish().finish_live_tree();
Some(())
}
}
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> {
/// Keep partial trees for errors inside the document element, but stop
/// before tree construction when the prologue itself is malformed.
fn note_lexical_errors(&self, source: &str) -> ControlFlow<()> {
let mut saw_document_element = false;
for token in xmlparser::Tokenizer::from(source) {
match token {
Ok(xmlparser::Token::ElementStart { .. }) => saw_document_element = true,
Err(error) => {
self.parse_error(Cow::Owned(error.to_string()));
return if saw_document_element {
ControlFlow::Continue(())
} else {
ControlFlow::Break(())
};
}
Ok(_) => {}
}
}
ControlFlow::Continue(())
}
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() == AsRef::<str>::as_ref(&attribute.name.ns)
&& existing.local_name() == AsRef::<str>::as_ref(&attribute.name.local)
&& 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_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))
);
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,
"<!DOCTYPE html PUBLIC \"public\"><html><div/></html>",
)
.unwrap();
assert!(host.child_handles(document).next().is_none());
assert!(!host.snapshot_document().parse_errors().is_empty());
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_rejects_malformed_attributes_without_rejecting_distinct_namespaces() {
let prologue = "<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'><body>";
for fragment in [
"<span novalue/>",
"<span =\"noattr\"/>",
"<span xmlns:='urn:test'/>",
"<span data-test=testing/>",
"<8:test xmlns:8='urn:test'/>",
"<span xmlns:p1 xmlns:p2='urn:test'/>",
] {
for source in [
fragment.to_owned(),
format!("{prologue}{fragment}</body></html>"),
] {
let dom = XmlParser.parse(
Url::parse("https://xml.test/malformed.xml").unwrap(),
source.clone(),
);
assert!(
!dom.parse_errors().is_empty(),
"{source} must report malformed XML"
);
}
}
let valid = format!(
"{prologue}<span data-test='testing' xmlns:p='urn:test' p:attr='x'/></body></html>"
);
let dom = XmlParser.parse(Url::parse("https://xml.test/valid.xml").unwrap(), valid);
assert!(dom.parse_errors().is_empty(), "{:?}", dom.parse_errors());
}
#[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_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_an_independent_inert_owner() {
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);
let inert = host
.node(inert_document)
.and_then(Node::as_document)
.unwrap();
assert_eq!(inert.url().as_str(), "about:blank");
assert_eq!(inert.base_url().as_str(), "about:blank");
assert!(!inert.is_html_document());
assert_eq!(
host.node(child_document)
.and_then(Node::as_document)
.map(|document| document.url()),
Some(&child_url)
);
let peer = host.create_element_without_attributes_for_document(
child_document,
"template".into(),
"http://www.w3.org/1999/xhtml".into(),
None,
);
let peer_contents = host
.node(peer)
.and_then(Node::as_element)
.and_then(|element| element.template_contents())
.unwrap();
assert_eq!(
host.owner_document_handle(peer_contents),
Some(inert_document)
);
}
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")
}
}