use html5_parser::{Document, NodeId as SourceNodeId, NodeKind as SourceNodeKind};
use crate::finding::SourceLocation;
pub(crate) const XHTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
pub(crate) const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
const CUSTOM_ELEMENT_NAMESPACE: &str = "http://n.validator.nu/custom-elements/";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExpandedName {
pub(crate) namespace: Option<String>,
pub(crate) local_name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct NodeId(usize);
#[derive(Debug, Clone, PartialEq, Eq)]
enum Kind {
Root,
Element {
name: ExpandedName,
attributes: Vec<NodeId>,
},
Attribute { name: ExpandedName, value: String },
Text { content: String },
Comment { content: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct NodeData {
kind: Kind,
position: Option<SourceLocation>,
parent: Option<NodeId>,
children: Vec<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NormalizedHtmlDocument {
nodes: Vec<NodeData>,
}
impl NormalizedHtmlDocument {
pub(crate) fn root(&self) -> NormalizedNode<'_> {
NormalizedNode {
document: self,
id: NodeId(0),
}
}
pub(crate) fn children(&self) -> impl Iterator<Item = NormalizedNode<'_>> {
self.root().child_nodes()
}
pub(crate) fn root_element(&self) -> Option<NormalizedNode<'_>> {
self.children()
.find(|node| matches!(node.data().kind, Kind::Element { .. }))
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct NormalizedNode<'a> {
document: &'a NormalizedHtmlDocument,
id: NodeId,
}
impl PartialEq for NormalizedNode<'_> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.document, other.document) && self.id == other.id
}
}
impl Eq for NormalizedNode<'_> {}
impl<'a> NormalizedNode<'a> {
fn data(self) -> &'a NodeData {
&self.document.nodes[self.id.0]
}
pub(crate) fn parent(self) -> Option<Self> {
self.data().parent.map(|id| NormalizedNode {
document: self.document,
id,
})
}
pub(crate) fn child_nodes(self) -> impl Iterator<Item = Self> + 'a {
self.data().children.iter().map(move |&id| NormalizedNode {
document: self.document,
id,
})
}
pub(crate) fn attribute_nodes(self) -> impl Iterator<Item = Self> + 'a {
let attributes: &'a [NodeId] = match &self.data().kind {
Kind::Element { attributes, .. } => attributes,
Kind::Root | Kind::Attribute { .. } | Kind::Text { .. } | Kind::Comment { .. } => &[],
};
attributes.iter().map(move |&id| NormalizedNode {
document: self.document,
id,
})
}
pub(crate) fn position(self) -> Option<&'a SourceLocation> {
self.data().position.as_ref()
}
#[allow(dead_code)]
pub(crate) fn is_document_root(self) -> bool {
self.id.0 == 0
}
}
pub(crate) fn normalize(document: &Document, source: &str) -> NormalizedHtmlDocument {
let _ = source;
let mut nodes = vec![NodeData {
kind: Kind::Root,
position: None,
parent: None,
children: Vec::new(),
}];
let root_id = NodeId(0);
let root_children = normalized_children(document, document.root(), &mut nodes, root_id);
nodes[0].children = root_children;
NormalizedHtmlDocument { nodes }
}
fn normalized_children(
document: &Document,
id: SourceNodeId,
nodes: &mut Vec<NodeData>,
parent: NodeId,
) -> Vec<NodeId> {
let mut result = Vec::new();
for child in document.children(id) {
if matches!(&document.node(child).kind, SourceNodeKind::DocumentFragment) {
result.extend(normalized_children(document, child, nodes, parent));
} else if let Some(normalized) = normalize_node(document, child, nodes, parent) {
result.push(normalized);
}
}
result
}
fn normalize_node(
document: &Document,
id: SourceNodeId,
nodes: &mut Vec<NodeData>,
parent: NodeId,
) -> Option<NodeId> {
if matches!(
&document.node(id).kind,
SourceNodeKind::Document
| SourceNodeKind::ProcessingInstruction { .. }
| SourceNodeKind::Doctype { .. }
) {
return None;
}
debug_assert!(
!matches!(&document.node(id).kind, SourceNodeKind::DocumentFragment),
"normalized_children should have intercepted any DocumentFragment before it reached here"
);
let position = document.node(id).position.map(|position| SourceLocation {
line: position.line,
column: position.column,
byte_offset: position.byte_offset,
});
let this_id = NodeId(nodes.len());
nodes.push(NodeData {
kind: Kind::Comment {
content: String::new(),
}, position,
parent: Some(parent),
children: Vec::new(),
});
let kind = match &document.node(id).kind {
SourceNodeKind::Element {
name,
namespace,
attributes,
} => Kind::Element {
name: ExpandedName {
namespace: Some(
namespace
.clone()
.unwrap_or_else(|| XHTML_NAMESPACE.to_owned()),
),
local_name: name.clone(),
},
attributes: attributes
.iter()
.map(|attribute| {
let attribute_id = NodeId(nodes.len());
nodes.push(NodeData {
kind: Kind::Attribute {
name: ExpandedName {
namespace: attribute.namespace.clone(),
local_name: attribute.name.clone(),
},
value: attribute.value.clone(),
},
position: None,
parent: Some(this_id),
children: Vec::new(),
});
attribute_id
})
.collect(),
},
SourceNodeKind::Text { content } => Kind::Text {
content: content.clone(),
},
SourceNodeKind::Comment { content } => Kind::Comment {
content: content.clone(),
},
SourceNodeKind::Document
| SourceNodeKind::ProcessingInstruction { .. }
| SourceNodeKind::Doctype { .. } => {
unreachable!("filtered out by the early check above")
}
SourceNodeKind::DocumentFragment => {
unreachable!("normalized_children intercepts DocumentFragment before normalize_node")
}
};
nodes[this_id.0].kind = kind;
let children = normalized_children(document, id, nodes, this_id);
nodes[this_id.0].children = children;
Some(this_id)
}
impl<'a> relax_ng::Element for NormalizedNode<'a> {
type Location = crate::finding::SourceLocation;
fn name(&self) -> relax_ng::ExpandedName {
match &self.data().kind {
Kind::Element { name, .. } => {
if name.namespace.as_deref() == Some(XHTML_NAMESPACE)
&& crate::datatypes::structural::check_custom_element_name(&name.local_name)
.is_ok()
{
relax_ng::ExpandedName {
namespace: Some(CUSTOM_ELEMENT_NAMESPACE.to_owned()),
local: name.local_name.clone(),
}
} else {
relax_ng::ExpandedName {
namespace: name.namespace.clone(),
local: name.local_name.clone(),
}
}
}
Kind::Text { .. } | Kind::Comment { .. } | Kind::Root | Kind::Attribute { .. } => {
panic!(
"relax_ng::Element::name() called on a non-Element NormalizedNode; \
only Content::Element(NormalizedNode::Element) should ever reach this"
)
}
}
}
fn attributes(&self) -> impl Iterator<Item = (relax_ng::ExpandedName, String)> {
let is_th = matches!(&self.data().kind, Kind::Element { name, .. }
if name.namespace.as_deref() == Some(XHTML_NAMESPACE) && name.local_name == "th");
let raw: Vec<(relax_ng::ExpandedName, String)> = (*self)
.attribute_nodes()
.map(|attribute| match &attribute.data().kind {
Kind::Attribute { name, value } => (
relax_ng::ExpandedName {
namespace: name.namespace.clone(),
local: name.local_name.clone(),
},
value.clone(),
),
Kind::Root | Kind::Element { .. } | Kind::Text { .. } | Kind::Comment { .. } => {
unreachable!("NormalizedNode::attribute_nodes only ever yields Attribute nodes")
}
})
.collect();
let has_literal_xml_lang = raw
.iter()
.any(|(name, _)| name.namespace.is_none() && name.local == "xml:lang");
let split_xml_lang_name = || relax_ng::ExpandedName {
namespace: Some(XML_NAMESPACE.to_owned()),
local: "lang".to_owned(),
};
raw.into_iter().filter_map(move |(name, value)| {
if name.namespace.is_none() && name.local == "xml:lang" {
Some((split_xml_lang_name(), value))
} else if name.namespace.is_none() && name.local == "lang" {
if has_literal_xml_lang {
None
} else {
Some((split_xml_lang_name(), value))
}
} else if name.namespace.as_deref() == Some(XMLNS_NAMESPACE)
|| (name.namespace.is_none()
&& (name.local == "xmlns"
|| name.local.starts_with("xmlns:")
|| (name.local.starts_with("data-") && name.local.len() > "data-".len())
|| (is_th && name.local == "abbr")))
{
None
} else {
Some((name, value))
}
})
}
fn children(&self) -> impl Iterator<Item = relax_ng::Content<Self>> {
let is_selectedcontent = matches!(&self.data().kind, Kind::Element { name, .. }
if name.namespace.as_deref() == Some(XHTML_NAMESPACE) && name.local_name == "selectedcontent");
if is_selectedcontent {
Vec::new().into_iter()
} else {
merge_text_and_comment_runs((*self).child_nodes()).into_iter()
}
}
fn location(&self) -> Option<Self::Location> {
self.position().copied()
}
}
fn merge_text_and_comment_runs<'a>(
children: impl Iterator<Item = NormalizedNode<'a>>,
) -> Vec<relax_ng::Content<NormalizedNode<'a>>> {
let mut result = Vec::new();
let mut pending_text: Option<String> = None;
for child in children {
match &child.data().kind {
Kind::Element { .. } => {
if let Some(text) = pending_text.take() {
result.push(relax_ng::Content::Text(text));
}
result.push(relax_ng::Content::Element(child));
}
Kind::Text { content } => match &mut pending_text {
Some(existing) => existing.push_str(content),
None => pending_text = Some(content.clone()),
},
Kind::Comment { .. } => {}
Kind::Root => unreachable!("the synthetic root is never anyone's child"),
Kind::Attribute { .. } => {
unreachable!("attribute nodes are never part of child_nodes()")
}
}
}
if let Some(text) = pending_text.take() {
result.push(relax_ng::Content::Text(text));
}
result
}
impl<'a> xpath_eval::Node<'a> for NormalizedNode<'a> {
fn kind(self) -> xpath_eval::NodeKind {
match &self.data().kind {
Kind::Root => xpath_eval::NodeKind::Root,
Kind::Element { .. } => xpath_eval::NodeKind::Element,
Kind::Attribute { .. } => xpath_eval::NodeKind::Attribute,
Kind::Text { .. } => xpath_eval::NodeKind::Text,
Kind::Comment { .. } => xpath_eval::NodeKind::Comment,
}
}
fn parent(self) -> Option<Self> {
NormalizedNode::parent(self)
}
fn children(self) -> impl Iterator<Item = Self> + 'a {
self.child_nodes()
}
fn attributes(self) -> impl Iterator<Item = Self> + 'a {
self.attribute_nodes()
}
fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn expanded_name(self) -> Option<xpath_eval::ExpandedName> {
let name = match &self.data().kind {
Kind::Element { name, .. } | Kind::Attribute { name, .. } => name,
Kind::Root | Kind::Text { .. } | Kind::Comment { .. } => return None,
};
Some(xpath_eval::ExpandedName {
namespace_uri: name.namespace.clone(),
local_name: name.local_name.clone(),
})
}
fn string_value(self) -> String {
match &self.data().kind {
Kind::Attribute { value, .. } => value.clone(),
Kind::Text { content } | Kind::Comment { content } => content.clone(),
Kind::Root | Kind::Element { .. } => {
let mut value = String::new();
collect_descendant_text(self, &mut value);
value
}
}
}
fn document_order(self, other: Self) -> std::cmp::Ordering {
self.id.0.cmp(&other.id.0)
}
}
fn collect_descendant_text(node: NormalizedNode<'_>, out: &mut String) {
match &node.data().kind {
Kind::Text { content } => out.push_str(content),
Kind::Root | Kind::Element { .. } => {
for child in node.child_nodes() {
collect_descendant_text(child, out);
}
}
Kind::Attribute { .. } | Kind::Comment { .. } => {}
}
}
impl xpath_eval::Document for NormalizedHtmlDocument {
type N<'a>
= NormalizedNode<'a>
where
Self: 'a;
fn root(&self) -> Self::N<'_> {
NormalizedHtmlDocument::root(self)
}
}
#[cfg(test)]
mod xpath_node_tests {
use xpath_eval::{Document, Node, NodeKind};
use super::{NormalizedHtmlDocument, normalize};
use crate::parse::parse;
fn normalize_html(html: &str) -> NormalizedHtmlDocument {
let parsed = parse(html);
normalize(parsed.document(), parsed.source())
}
fn element_children<'a>(
node: super::NormalizedNode<'a>,
) -> impl Iterator<Item = super::NormalizedNode<'a>> {
node.children().filter(|n| n.kind() == NodeKind::Element)
}
fn html_element(document: &NormalizedHtmlDocument) -> super::NormalizedNode<'_> {
element_children(document.root())
.next()
.expect("expected <html>")
}
fn body_element(document: &NormalizedHtmlDocument) -> super::NormalizedNode<'_> {
element_children(html_element(document))
.nth(1)
.expect("expected <body> as html's second element child")
}
#[test]
fn root_kind_and_parentless() {
let document = normalize_html("<p>hi</p>");
let root = document.root();
assert_eq!(root.kind(), NodeKind::Root);
assert_eq!(root.parent(), None);
}
#[test]
fn document_root_is_reachable_through_the_trait_too() {
let document = normalize_html("<p>hi</p>");
assert_eq!(Document::root(&document), document.root());
}
#[test]
fn satisfies_the_node_trait_bound_generically() {
fn generic_kind<'a, N: Node<'a>>(node: N) -> NodeKind {
node.kind()
}
let document = normalize_html("<p>hi</p>");
assert_eq!(generic_kind(document.root()), NodeKind::Root);
}
#[test]
fn element_kind_and_expanded_name() {
let document = normalize_html("<p>hi</p>");
let html = html_element(&document);
assert_eq!(html.kind(), NodeKind::Element);
let name = html.expanded_name().expect("element should have a name");
assert_eq!(name.namespace_uri.as_deref(), Some(super::XHTML_NAMESPACE));
assert_eq!(name.local_name, "html");
}
#[test]
fn attribute_node_kind_name_value_and_parent() {
let document = normalize_html(r#"<p id="x">hi</p>"#);
let body = body_element(&document);
let p = element_children(body).next().expect("expected <p>");
let attribute = p
.attributes()
.next()
.expect("<p id=\"x\"> should have one attribute node");
assert_eq!(attribute.kind(), NodeKind::Attribute);
let name = attribute
.expanded_name()
.expect("attribute should have a name");
assert_eq!(name.local_name, "id");
assert_eq!(attribute.string_value(), "x");
assert_eq!(attribute.parent(), Some(p));
}
#[test]
fn text_node_string_value_is_its_content() {
let document = normalize_html("<p>hi</p>");
let body = body_element(&document);
let p = element_children(body).next().expect("expected <p>");
let text = p
.children()
.find(|n| n.kind() == NodeKind::Text)
.expect("expected a text child");
assert_eq!(text.string_value(), "hi");
}
#[test]
fn comment_node_string_value_is_its_content() {
let document = normalize_html("<p><!--hello--></p>");
let body = body_element(&document);
let p = element_children(body).next().expect("expected <p>");
let comment = p
.children()
.find(|n| n.kind() == NodeKind::Comment)
.expect("expected a comment child");
assert_eq!(comment.string_value(), "hello");
}
#[test]
fn element_string_value_concatenates_descendant_text_and_skips_comments() {
let document = normalize_html("<div>a<!--x-->b<span>c</span></div>");
let body = body_element(&document);
let div = element_children(body).next().expect("expected <div>");
assert_eq!(div.string_value(), "abc");
}
#[test]
fn namespaces_are_always_empty() {
let document = normalize_html("<p>hi</p>");
let html = html_element(&document);
assert_eq!(html.namespaces().count(), 0);
}
#[test]
fn document_order_matches_arena_build_order() {
let document = normalize_html(r#"<p id="x">hi</p>"#);
let html = html_element(&document);
let body = body_element(&document);
let p = element_children(body).next().expect("expected <p>");
let attribute = p.attributes().next().expect("expected id attribute");
let text = p
.children()
.find(|n| n.kind() == NodeKind::Text)
.expect("expected text child");
use std::cmp::Ordering;
assert_eq!(html.document_order(body), Ordering::Less);
assert_eq!(p.document_order(attribute), Ordering::Less);
assert_eq!(attribute.document_order(text), Ordering::Less);
assert_eq!(p.document_order(p), Ordering::Equal);
}
}
#[cfg(test)]
mod tests {
use super::{
CUSTOM_ELEMENT_NAMESPACE, MATHML_NAMESPACE, NormalizedHtmlDocument, NormalizedNode,
SVG_NAMESPACE, XHTML_NAMESPACE, XML_NAMESPACE, normalize,
};
use crate::parse::parse;
fn normalize_html(html: &str) -> NormalizedHtmlDocument {
let parsed = parse(html);
normalize(parsed.document(), parsed.source())
}
fn only_element<'a>(document: &'a NormalizedHtmlDocument) -> NormalizedNode<'a> {
let mut children = document.children();
let only = children.next().expect("document should have one child");
assert!(children.next().is_none(), "expected exactly one child");
only
}
fn expect_element<'a>(
node: NormalizedNode<'a>,
namespace: &str,
local_name: &str,
) -> Vec<NormalizedNode<'a>> {
use relax_ng::Element;
assert_eq!(node.name().namespace.as_deref(), Some(namespace));
assert_eq!(node.name().local, local_name);
node.child_nodes().collect()
}
fn find_element<'a>(children: &[NormalizedNode<'a>], local_name: &str) -> NormalizedNode<'a> {
use relax_ng::Element;
*children
.iter()
.find(|child| child.name().local == local_name)
.unwrap_or_else(|| panic!("expected to find element {local_name:?}"))
}
fn expect_text(node: NormalizedNode<'_>, expected: &str) {
match &node.data().kind {
super::Kind::Text { content } => assert_eq!(content, expected),
other => panic!("expected text {expected:?}, got {other:?}"),
}
}
#[test]
fn document_root_is_document_root() {
let document = normalize_html("<p>hi</p>");
let html = only_element(&document);
assert!(!html.is_document_root());
let root = document.children().next().unwrap();
assert_eq!(root.id.0, 1);
}
#[test]
fn implicit_html_head_body_are_synthesized_with_xhtml_namespace() {
use relax_ng::Element;
let document = normalize_html("<p>hi</p>");
let html = only_element(&document);
assert_eq!(html.name().local, "html");
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let head = find_element(&html_children, "head");
assert_eq!(head.name().local, "head");
expect_element(head, XHTML_NAMESPACE, "head");
let body = find_element(&html_children, "body");
assert_eq!(body.name().local, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
assert_eq!(p.name().local, "p");
let p_children = expect_element(p, XHTML_NAMESPACE, "p");
expect_text(p_children[0], "hi");
}
#[test]
fn optional_end_tags_produce_sibling_elements() {
let document = normalize_html("<ul><li>a<li>b</ul>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let ul = find_element(&body_children, "ul");
let ul_children = expect_element(ul, XHTML_NAMESPACE, "ul");
use relax_ng::Element;
let li_elements: Vec<_> = ul_children
.iter()
.filter(|child| child.name().local == "li")
.collect();
assert_eq!(li_elements.len(), 2);
let first_li_children = expect_element(*li_elements[0], XHTML_NAMESPACE, "li");
expect_text(first_li_children[0], "a");
let second_li_children = expect_element(*li_elements[1], XHTML_NAMESPACE, "li");
expect_text(second_li_children[0], "b");
}
#[test]
fn svg_elements_keep_svg_namespace() {
use relax_ng::Element;
let document = normalize_html("<svg><circle/></svg>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let svg = find_element(&body_children, "svg");
assert_eq!(svg.name().local, "svg");
let svg_children = expect_element(svg, SVG_NAMESPACE, "svg");
let circle = find_element(&svg_children, "circle");
assert_eq!(circle.name().local, "circle");
expect_element(circle, SVG_NAMESPACE, "circle");
}
#[test]
fn mathml_elements_keep_mathml_namespace() {
use relax_ng::Element;
let document = normalize_html("<math><mi>x</mi></math>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let math = find_element(&body_children, "math");
assert_eq!(math.name().local, "math");
let math_children = expect_element(math, MATHML_NAMESPACE, "math");
let mi = find_element(&math_children, "mi");
assert_eq!(mi.name().local, "mi");
let mi_children = expect_element(mi, MATHML_NAMESPACE, "mi");
expect_text(mi_children[0], "x");
}
#[test]
fn script_and_style_content_normalize_to_plain_text() {
use relax_ng::Element;
let script_document = normalize_html("<script>1 < 2;</script>");
let script_html = only_element(&script_document);
let script_html_children = expect_element(script_html, XHTML_NAMESPACE, "html");
let head = find_element(&script_html_children, "head");
let head_children = expect_element(head, XHTML_NAMESPACE, "head");
let script = find_element(&head_children, "script");
assert_eq!(script.name().local, "script");
let script_children = expect_element(script, XHTML_NAMESPACE, "script");
expect_text(script_children[0], "1 < 2;");
let style_document = normalize_html("<style>a{color:red}</style>");
let style_html = only_element(&style_document);
let style_html_children = expect_element(style_html, XHTML_NAMESPACE, "html");
let style_head = find_element(&style_html_children, "head");
let style_head_children = expect_element(style_head, XHTML_NAMESPACE, "head");
let style = find_element(&style_head_children, "style");
assert_eq!(style.name().local, "style");
let style_children = expect_element(style, XHTML_NAMESPACE, "style");
expect_text(style_children[0], "a{color:red}");
}
#[test]
fn named_entities_resolve_to_decoded_text() {
use relax_ng::Element;
let document = normalize_html("<p>& ©</p>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
assert_eq!(p.name().local, "p");
let p_children = expect_element(p, XHTML_NAMESPACE, "p");
expect_text(p_children[0], "& \u{a9}");
}
#[test]
fn xml_lang_attribute_keeps_literal_local_name_on_html_elements() {
let document = normalize_html(r#"<p xml:lang="de">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attribute = p
.attribute_nodes()
.next()
.expect("p should have one attribute");
assert_eq!(
xpath_eval::Node::expanded_name(attribute),
Some(xpath_eval::ExpandedName {
namespace_uri: None,
local_name: "xml:lang".to_owned(),
})
);
}
#[test]
fn schema_layer_remaps_plain_lang_to_the_split_xml_namespace_form() {
use relax_ng::Element;
let document = normalize_html(r#"<p lang="de">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attributes: Vec<_> = p.attributes().collect();
assert_eq!(
attributes,
vec![(
relax_ng::ExpandedName {
namespace: Some(XML_NAMESPACE.to_owned()),
local: "lang".to_owned(),
},
"de".to_owned(),
)]
);
}
#[test]
fn schema_layer_remaps_literal_xml_lang_to_the_split_xml_namespace_form() {
use relax_ng::Element;
let document = normalize_html(r#"<p xml:lang="de">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attributes: Vec<_> = p.attributes().collect();
assert_eq!(
attributes,
vec![(
relax_ng::ExpandedName {
namespace: Some(XML_NAMESPACE.to_owned()),
local: "lang".to_owned(),
},
"de".to_owned(),
)]
);
}
#[test]
fn schema_layer_prefers_literal_xml_lang_value_over_plain_lang_when_both_present() {
use relax_ng::Element;
let document = normalize_html(r#"<p lang="de" xml:lang="fr">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attributes: Vec<_> = p.attributes().collect();
assert_eq!(
attributes,
vec![(
relax_ng::ExpandedName {
namespace: Some(XML_NAMESPACE.to_owned()),
local: "lang".to_owned(),
},
"fr".to_owned(),
)]
);
}
#[test]
fn schema_layer_drops_data_star_attributes() {
use relax_ng::Element;
let document = normalize_html(r#"<p data-z="" data-z:foo="">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attributes: Vec<_> = p.attributes().collect();
assert_eq!(attributes, Vec::new());
}
#[test]
fn schema_layer_drops_xmlns_attributes() {
use relax_ng::Element;
let document = normalize_html(
r#"<svg xmlns="http://www.w3.org/2000/svg"><foreignObject><div xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>"#,
);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let svg = find_element(&body_children, "svg");
assert_eq!(svg.attributes().collect::<Vec<_>>(), Vec::new());
let svg_children = expect_element(svg, SVG_NAMESPACE, "svg");
let foreign_object = find_element(&svg_children, "foreignObject");
let foreign_object_children =
expect_element(foreign_object, SVG_NAMESPACE, "foreignObject");
let div = find_element(&foreign_object_children, "div");
assert_eq!(div.attributes().collect::<Vec<_>>(), Vec::new());
}
#[test]
fn schema_layer_keeps_bare_data_hyphen_attribute() {
use relax_ng::Element;
let document = normalize_html(r#"<p data-="">hi</p>"#);
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let attributes: Vec<_> = p.attributes().collect();
assert_eq!(
attributes,
vec![(
relax_ng::ExpandedName {
namespace: None,
local: "data-".to_owned(),
},
String::new(),
)]
);
}
#[test]
fn schema_layer_validates_svg_subtree_for_real() {
use relax_ng::Element;
let document = normalize_html("<p>before<svg><path/><circle/></svg>after</p>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let children: Vec<_> = p.children().collect();
assert_eq!(children.len(), 3, "expected before-text, svg, after-text");
assert_eq!(children[0], relax_ng::Content::Text("before".to_owned()));
assert!(matches!(children[1], relax_ng::Content::Element(_)));
assert_eq!(children[2], relax_ng::Content::Text("after".to_owned()));
let relax_ng::Content::Element(svg) = children[1] else {
unreachable!("just matched Content::Element above");
};
assert_eq!(svg.name().namespace.as_deref(), Some(SVG_NAMESPACE));
assert_eq!(svg.name().local, "svg");
}
#[test]
fn schema_layer_validates_mathml_subtree_for_real() {
use relax_ng::Element;
let document = normalize_html("<p>before<math><mi>x</mi></math>after</p>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
let children: Vec<_> = p.children().collect();
assert_eq!(children.len(), 3, "expected before-text, math, after-text");
assert_eq!(children[0], relax_ng::Content::Text("before".to_owned()));
assert!(matches!(children[1], relax_ng::Content::Element(_)));
assert_eq!(children[2], relax_ng::Content::Text("after".to_owned()));
let relax_ng::Content::Element(math) = children[1] else {
unreachable!("just matched Content::Element above");
};
assert_eq!(math.name().namespace.as_deref(), Some(MATHML_NAMESPACE));
assert_eq!(math.name().local, "math");
}
#[test]
fn custom_element_keeps_xhtml_namespace_for_xpath_but_not_schema() {
let document = normalize_html("<my-widget>hi</my-widget>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let widget = find_element(&body_children, "my-widget");
assert_eq!(
xpath_eval::Node::expanded_name(widget),
Some(xpath_eval::ExpandedName {
namespace_uri: Some(XHTML_NAMESPACE.to_owned()),
local_name: "my-widget".to_owned(),
})
);
let widget_children: Vec<_> = xpath_eval::Node::children(widget).collect();
expect_text(widget_children[0], "hi");
}
#[test]
fn custom_element_gets_the_vnu_custom_element_namespace_for_schema() {
use relax_ng::Element;
let document = normalize_html("<my-widget>hi</my-widget>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let widget = find_element(&body_children, "my-widget");
assert_eq!(
widget.name().namespace.as_deref(),
Some(CUSTOM_ELEMENT_NAMESPACE)
);
assert_eq!(widget.name().local, "my-widget");
}
#[test]
fn plain_element_with_a_hyphenless_name_keeps_xhtml_namespace_for_schema_too() {
use relax_ng::Element;
let document = normalize_html("<p>hi</p>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
assert_eq!(p.name().namespace.as_deref(), Some(XHTML_NAMESPACE));
}
#[test]
fn synthesized_nodes_have_no_position_but_explicit_ones_do() {
let document = normalize_html("<p>hi</p>");
let html = only_element(&document);
assert!(html.position().is_none(), "implicit <html> has no position");
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let head = find_element(&html_children, "head");
assert!(head.position().is_none(), "implicit <head> has no position");
let body = find_element(&html_children, "body");
assert!(body.position().is_none(), "implicit <body> has no position");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let p = find_element(&body_children, "p");
assert!(
p.position().is_some(),
"explicit <p> should have a position"
);
let p_children = expect_element(p, XHTML_NAMESPACE, "p");
let text = p_children[0];
assert!(
text.position().is_some(),
"explicit text content should have a position"
);
}
#[test]
fn parent_of_top_level_node_is_the_synthetic_root() {
let document = normalize_html("<p>hi</p>");
let html = only_element(&document);
let parent = html.parent().expect("html should have a parent");
assert!(parent.is_document_root());
assert!(parent.parent().is_none(), "the root itself has no parent");
}
#[test]
fn parent_of_nested_element_round_trips_to_the_child() {
use relax_ng::Element;
let document = normalize_html("<div><p>hi</p></div>");
let html = only_element(&document);
let html_children = expect_element(html, XHTML_NAMESPACE, "html");
let body = find_element(&html_children, "body");
let body_children = expect_element(body, XHTML_NAMESPACE, "body");
let div = find_element(&body_children, "div");
let div_children: Vec<_> = div.child_nodes().collect();
let p = find_element(&div_children, "p");
let parent = p.parent().expect("p should have a parent");
assert_eq!(parent.name().local, "div");
}
}
#[cfg(test)]
mod element_adapter_tests {
use relax_ng::{Content, Element};
use super::{NormalizedHtmlDocument, NormalizedNode, normalize};
use crate::parse::parse;
fn normalize_html(html: &str) -> NormalizedHtmlDocument {
let parsed = parse(html);
normalize(parsed.document(), parsed.source())
}
fn find_by_local_name<'a>(node: NormalizedNode<'a>, local_name: &str) -> NormalizedNode<'a> {
fn search<'a>(node: NormalizedNode<'a>, local_name: &str) -> Option<NormalizedNode<'a>> {
if matches!(&node.data().kind, super::Kind::Element{ name, .. } if name.local_name == local_name)
{
return Some(node);
}
node.child_nodes()
.find_map(|child| search(child, local_name))
}
search(node, local_name)
.unwrap_or_else(|| panic!("expected to find element {local_name:?}"))
}
fn first_child(document: &NormalizedHtmlDocument) -> NormalizedNode<'_> {
document
.children()
.next()
.expect("document should have a child")
}
#[test]
fn element_with_child_elements_yields_content_element_per_child() {
let document = normalize_html("<div><p>a</p><p>b</p></div>");
let div = find_by_local_name(first_child(&document), "div");
let content: Vec<_> = div.children().collect();
assert_eq!(content.len(), 2);
for item in &content {
match item {
Content::Element(element) => assert_eq!(element.name().local, "p"),
Content::Text(text) => panic!("expected element child, got text {text:?}"),
}
}
}
#[test]
fn element_with_text_content_yields_content_text() {
let document = normalize_html("<p>hi</p>");
let p = find_by_local_name(first_child(&document), "p");
let content: Vec<_> = p.children().collect();
assert_eq!(content, vec![Content::Text("hi".to_owned())]);
}
#[test]
fn mixed_content_yields_correct_content_sequence() {
let document = normalize_html("<p>a<b>x</b>c</p>");
let p = find_by_local_name(first_child(&document), "p");
let content: Vec<_> = p.children().collect();
assert_eq!(content.len(), 3);
assert_eq!(content[0], Content::Text("a".to_owned()));
match &content[1] {
Content::Element(element) => assert_eq!(element.name().local, "b"),
other => panic!("expected element child, got {other:?}"),
}
assert_eq!(content[2], Content::Text("c".to_owned()));
}
#[test]
fn comment_child_is_skipped_and_surrounding_text_is_merged() {
let document = normalize_html("<p>a<!--x-->b</p>");
let p = find_by_local_name(first_child(&document), "p");
let underlying_child_count = p.child_nodes().count();
assert_eq!(underlying_child_count, 3, "expected Text, Comment, Text");
let content: Vec<_> = p.children().collect();
assert_eq!(content, vec![Content::Text("ab".to_owned())]);
}
#[test]
fn comment_between_elements_with_no_adjacent_text_yields_nothing() {
let document = normalize_html("<div><p>a</p><!--x--><p>b</p></div>");
let div = find_by_local_name(first_child(&document), "div");
let underlying_child_count = div.child_nodes().count();
assert_eq!(
underlying_child_count, 3,
"expected Element, Comment, Element"
);
let content: Vec<_> = div.children().collect();
assert_eq!(
content.len(),
2,
"the comment must not produce an empty Content::Text"
);
for item in &content {
match item {
Content::Element(element) => assert_eq!(element.name().local, "p"),
other => panic!("expected only element children, got {other:?}"),
}
}
}
#[test]
fn attributes_map_namespace_and_local_name() {
let document = normalize_html(r#"<a xlink:href="https://example.com">x</a>"#);
let a = find_by_local_name(first_child(&document), "a");
let attributes: Vec<_> = a.attributes().collect();
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].0.local, "xlink:href");
assert_eq!(attributes[0].1, "https://example.com");
}
#[test]
fn location_is_some_for_an_explicit_element_since_the_html5_parser_migration() {
let document = normalize_html("<p>hi</p>");
let p = find_by_local_name(first_child(&document), "p");
assert_eq!(
p.location(),
Some(crate::finding::SourceLocation {
line: 1,
column: 1,
byte_offset: 0,
})
);
}
#[test]
fn normalize_never_produces_two_adjacent_text_siblings() {
let document = normalize_html("<p>a<!--x-->b</p>");
let p = find_by_local_name(first_child(&document), "p");
fn assert_no_adjacent_text_siblings(node: NormalizedNode<'_>) {
let children: Vec<_> = node.child_nodes().collect();
for window in children.windows(2) {
assert!(
!matches!(
(&window[0].data().kind, &window[1].data().kind),
(super::Kind::Text { .. }, super::Kind::Text { .. })
),
"found two adjacent Text siblings"
);
}
for child in children {
assert_no_adjacent_text_siblings(child);
}
}
assert_no_adjacent_text_siblings(p);
}
}