use crate::document::{Attribute, Document, NodeId, NodeKind};
use crate::tokenizer::{DoctypeToken, ExternalState, Position, TagToken, TokenKind};
pub(crate) const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
pub(crate) const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
pub(crate) const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
const XLINK_NAMESPACE: &str = "http://www.w3.org/1999/xlink";
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
const FOREIGN_ATTRIBUTE_NAMESPACES: &[(&str, &str)] = &[
("xlink:actuate", XLINK_NAMESPACE),
("xlink:arcrole", XLINK_NAMESPACE),
("xlink:href", XLINK_NAMESPACE),
("xlink:role", XLINK_NAMESPACE),
("xlink:show", XLINK_NAMESPACE),
("xlink:title", XLINK_NAMESPACE),
("xlink:type", XLINK_NAMESPACE),
("xml:lang", XML_NAMESPACE),
("xml:space", XML_NAMESPACE),
("xmlns", XMLNS_NAMESPACE),
("xmlns:xlink", XMLNS_NAMESPACE),
];
const SVG_ATTRIBUTE_ADJUSTMENTS: &[(&str, &str)] = &[
("attributename", "attributeName"),
("attributetype", "attributeType"),
("basefrequency", "baseFrequency"),
("baseprofile", "baseProfile"),
("calcmode", "calcMode"),
("clippathunits", "clipPathUnits"),
("diffuseconstant", "diffuseConstant"),
("edgemode", "edgeMode"),
("filterunits", "filterUnits"),
("glyphref", "glyphRef"),
("gradienttransform", "gradientTransform"),
("gradientunits", "gradientUnits"),
("kernelmatrix", "kernelMatrix"),
("kernelunitlength", "kernelUnitLength"),
("keypoints", "keyPoints"),
("keysplines", "keySplines"),
("keytimes", "keyTimes"),
("lengthadjust", "lengthAdjust"),
("limitingconeangle", "limitingConeAngle"),
("markerheight", "markerHeight"),
("markerunits", "markerUnits"),
("markerwidth", "markerWidth"),
("maskcontentunits", "maskContentUnits"),
("maskunits", "maskUnits"),
("numoctaves", "numOctaves"),
("pathlength", "pathLength"),
("patterncontentunits", "patternContentUnits"),
("patterntransform", "patternTransform"),
("patternunits", "patternUnits"),
("pointsatx", "pointsAtX"),
("pointsaty", "pointsAtY"),
("pointsatz", "pointsAtZ"),
("preservealpha", "preserveAlpha"),
("preserveaspectratio", "preserveAspectRatio"),
("primitiveunits", "primitiveUnits"),
("refx", "refX"),
("refy", "refY"),
("repeatcount", "repeatCount"),
("repeatdur", "repeatDur"),
("requiredextensions", "requiredExtensions"),
("requiredfeatures", "requiredFeatures"),
("specularconstant", "specularConstant"),
("specularexponent", "specularExponent"),
("spreadmethod", "spreadMethod"),
("startoffset", "startOffset"),
("stddeviation", "stdDeviation"),
("stitchtiles", "stitchTiles"),
("surfacescale", "surfaceScale"),
("systemlanguage", "systemLanguage"),
("tablevalues", "tableValues"),
("targetx", "targetX"),
("targety", "targetY"),
("textlength", "textLength"),
("viewbox", "viewBox"),
("viewtarget", "viewTarget"),
("xchannelselector", "xChannelSelector"),
("ychannelselector", "yChannelSelector"),
("zoomandpan", "zoomAndPan"),
];
const MATHML_ATTRIBUTE_ADJUSTMENTS: &[(&str, &str)] = &[("definitionurl", "definitionURL")];
fn adjust_attributes_for_foreign_element(tag: &TagToken, namespace: &str) -> Vec<Attribute> {
let name_adjustments: &[(&str, &str)] = if namespace == MATHML_NAMESPACE {
MATHML_ATTRIBUTE_ADJUSTMENTS
} else if namespace == SVG_NAMESPACE {
SVG_ATTRIBUTE_ADJUSTMENTS
} else {
&[]
};
tag.attributes
.iter()
.map(|attribute| {
let name = name_adjustments
.iter()
.find(|&&(from, _)| from == attribute.name)
.map_or_else(|| attribute.name.clone(), |&(_, to)| to.to_owned());
let namespace = FOREIGN_ATTRIBUTE_NAMESPACES
.iter()
.find(|&&(from, _)| from == name)
.map(|&(_, ns)| ns.to_owned());
Attribute {
name,
value: attribute.value.clone(),
namespace,
}
})
.collect()
}
const SVG_TAG_NAME_ADJUSTMENTS: &[(&str, &str)] = &[
("altglyph", "altGlyph"),
("altglyphdef", "altGlyphDef"),
("altglyphitem", "altGlyphItem"),
("animatecolor", "animateColor"),
("animatemotion", "animateMotion"),
("animatetransform", "animateTransform"),
("clippath", "clipPath"),
("feblend", "feBlend"),
("fecolormatrix", "feColorMatrix"),
("fecomponenttransfer", "feComponentTransfer"),
("fecomposite", "feComposite"),
("feconvolvematrix", "feConvolveMatrix"),
("fediffuselighting", "feDiffuseLighting"),
("fedisplacementmap", "feDisplacementMap"),
("fedistantlight", "feDistantLight"),
("fedropshadow", "feDropShadow"),
("feflood", "feFlood"),
("fefunca", "feFuncA"),
("fefuncb", "feFuncB"),
("fefuncg", "feFuncG"),
("fefuncr", "feFuncR"),
("fegaussianblur", "feGaussianBlur"),
("feimage", "feImage"),
("femerge", "feMerge"),
("femergenode", "feMergeNode"),
("femorphology", "feMorphology"),
("feoffset", "feOffset"),
("fepointlight", "fePointLight"),
("fespecularlighting", "feSpecularLighting"),
("fespotlight", "feSpotLight"),
("fetile", "feTile"),
("feturbulence", "feTurbulence"),
("foreignobject", "foreignObject"),
("glyphref", "glyphRef"),
("lineargradient", "linearGradient"),
("radialgradient", "radialGradient"),
("textpath", "textPath"),
];
fn adjust_svg_tag_name(name: &str) -> &str {
SVG_TAG_NAME_ADJUSTMENTS
.iter()
.find(|&&(from, _)| from == name)
.map_or(name, |&(_, to)| to)
}
fn is_mathml_text_integration_point(document: &Document, node: NodeId) -> bool {
let NodeKind::Element {
name, namespace, ..
} = &document.node(node).kind
else {
return false;
};
namespace.as_deref() == Some(MATHML_NAMESPACE)
&& matches!(name.as_str(), "mi" | "mo" | "mn" | "ms" | "mtext")
}
fn is_html_integration_point(document: &Document, node: NodeId) -> bool {
let NodeKind::Element {
name,
namespace,
attributes,
} = &document.node(node).kind
else {
return false;
};
match namespace.as_deref() {
Some(MATHML_NAMESPACE) if name == "annotation-xml" => attributes.iter().any(|attribute| {
attribute.name == "encoding"
&& (attribute.value.eq_ignore_ascii_case("text/html")
|| attribute
.value
.eq_ignore_ascii_case("application/xhtml+xml"))
}),
Some(SVG_NAMESPACE) => matches!(name.as_str(), "foreignObject" | "desc" | "title"),
_ => false,
}
}
#[derive(Debug, Clone, Copy)]
struct ElementType {
namespace: &'static str,
name: &'static str,
}
fn element_type_matches(list: &[ElementType], namespace: &str, name: &str) -> bool {
list.iter()
.any(|element_type| element_type.namespace == namespace && element_type.name == name)
}
const DEFAULT_SCOPE: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "applet",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "caption",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "html",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "table",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "td",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "th",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "marquee",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "object",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "select",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "template",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mi",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mo",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mn",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "ms",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mtext",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "annotation-xml",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "foreignObject",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "desc",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "title",
},
];
const LIST_ITEM_SCOPE_EXTRA: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "ol",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "ul",
},
];
const BUTTON_SCOPE_EXTRA: &[ElementType] = &[ElementType {
namespace: HTML_NAMESPACE,
name: "button",
}];
const TABLE_SCOPE: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "html",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "table",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "template",
},
];
const SPECIAL_CATEGORY: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "address",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "applet",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "area",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "article",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "aside",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "base",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "basefont",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "bgsound",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "blockquote",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "body",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "br",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "button",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "caption",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "center",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "col",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "colgroup",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dd",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "details",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dir",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "div",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dl",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dt",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "embed",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "fieldset",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "figcaption",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "figure",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "footer",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "form",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "frame",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "frameset",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h1",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h2",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h3",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h4",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h5",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "h6",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "head",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "header",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "hgroup",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "hr",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "html",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "iframe",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "img",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "input",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "keygen",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "li",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "link",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "listing",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "main",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "marquee",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "menu",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "meta",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "nav",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "noembed",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "noframes",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "noscript",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "object",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "ol",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "p",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "param",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "plaintext",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "pre",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "script",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "search",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "section",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "select",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "source",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "style",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "summary",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "table",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tbody",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "td",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "template",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "textarea",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tfoot",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "th",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "thead",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "title",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tr",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "track",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "ul",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "wbr",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "xmp",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mi",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mo",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mn",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "ms",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "mtext",
},
ElementType {
namespace: MATHML_NAMESPACE,
name: "annotation-xml",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "foreignObject",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "desc",
},
ElementType {
namespace: SVG_NAMESPACE,
name: "title",
},
];
fn is_special(document: &Document, node: NodeId) -> bool {
let NodeKind::Element {
name, namespace, ..
} = &document.node(node).kind
else {
return false;
};
element_type_matches(SPECIAL_CATEGORY, namespace.as_deref().unwrap_or(""), name)
}
const IMPLIED_END_TAGS: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "dd",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dt",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "li",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "optgroup",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "option",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "p",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rb",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rp",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rt",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rtc",
},
];
const IMPLIED_END_TAGS_THOROUGHLY: &[ElementType] = &[
ElementType {
namespace: HTML_NAMESPACE,
name: "caption",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "colgroup",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dd",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "dt",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "li",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "optgroup",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "option",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "p",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rb",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rp",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rt",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "rtc",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tbody",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "td",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tfoot",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "th",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "thead",
},
ElementType {
namespace: HTML_NAMESPACE,
name: "tr",
},
];
#[derive(Debug, Default)]
pub(crate) struct OpenElementsStack {
entries: Vec<NodeId>,
}
impl OpenElementsStack {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, node: NodeId) {
self.entries.push(node);
}
pub(crate) fn pop(&mut self) -> Option<NodeId> {
self.entries.pop()
}
pub(crate) fn current_node(&self) -> Option<NodeId> {
self.entries.last().copied()
}
pub(crate) fn contains(&self, node: NodeId) -> bool {
self.entries.contains(&node)
}
fn has_element_in_specific_scope(
&self,
document: &Document,
target_namespace: &str,
target_name: &str,
boundary: &[ElementType],
extra_boundary: &[ElementType],
) -> bool {
for &node in self.entries.iter().rev() {
let NodeKind::Element {
name, namespace, ..
} = &document.node(node).kind
else {
continue;
};
let namespace = namespace.as_deref().unwrap_or("");
if namespace == target_namespace && name == target_name {
return true;
}
if element_type_matches(boundary, namespace, name)
|| element_type_matches(extra_boundary, namespace, name)
{
return false;
}
}
false
}
pub(crate) fn has_element_in_scope(&self, document: &Document, name: &str) -> bool {
self.has_element_in_specific_scope(document, HTML_NAMESPACE, name, DEFAULT_SCOPE, &[])
}
pub(crate) fn has_element_in_list_item_scope(&self, document: &Document, name: &str) -> bool {
self.has_element_in_specific_scope(
document,
HTML_NAMESPACE,
name,
DEFAULT_SCOPE,
LIST_ITEM_SCOPE_EXTRA,
)
}
pub(crate) fn has_element_in_button_scope(&self, document: &Document, name: &str) -> bool {
self.has_element_in_specific_scope(
document,
HTML_NAMESPACE,
name,
DEFAULT_SCOPE,
BUTTON_SCOPE_EXTRA,
)
}
pub(crate) fn has_element_in_table_scope(&self, document: &Document, name: &str) -> bool {
self.has_element_in_specific_scope(document, HTML_NAMESPACE, name, TABLE_SCOPE, &[])
}
fn topmost(&self) -> Option<NodeId> {
self.entries.first().copied()
}
fn last_matching(&self, document: &Document, namespace: &str, name: &str) -> Option<NodeId> {
self.entries.iter().rev().copied().find(|&node| {
let NodeKind::Element {
name: node_name,
namespace: node_namespace,
..
} = &document.node(node).kind
else {
return false;
};
node_namespace.as_deref() == Some(namespace) && node_name == name
})
}
fn is_lower(&self, a: NodeId, b: NodeId) -> bool {
let position_of = |target: NodeId| {
self.entries
.iter()
.position(|&node| node == target)
.expect("is_lower called with a node that isn't on the stack")
};
position_of(a) > position_of(b)
}
fn element_immediately_above(&self, node: NodeId) -> Option<NodeId> {
let position = self.entries.iter().position(|&entry| entry == node)?;
position.checked_sub(1).map(|above| self.entries[above])
}
fn topmost_special_element_below(
&self,
document: &Document,
formatting_element: NodeId,
) -> Option<NodeId> {
let position = self
.entries
.iter()
.position(|&entry| entry == formatting_element)?;
self.entries[position + 1..]
.iter()
.copied()
.find(|&node| is_special(document, node))
}
pub(crate) fn generate_implied_end_tags(&mut self, document: &Document, exclude: Option<&str>) {
self.generate_implied_end_tags_impl(document, IMPLIED_END_TAGS, exclude);
}
pub(crate) fn generate_implied_end_tags_thoroughly(&mut self, document: &Document) {
self.generate_implied_end_tags_impl(document, IMPLIED_END_TAGS_THOROUGHLY, None);
}
fn generate_implied_end_tags_impl(
&mut self,
document: &Document,
list: &[ElementType],
exclude: Option<&str>,
) {
while let Some(node) = self.current_node() {
let NodeKind::Element {
name, namespace, ..
} = &document.node(node).kind
else {
break;
};
let namespace = namespace.as_deref().unwrap_or("");
if Some(name.as_str()) == exclude {
break;
}
if !element_type_matches(list, namespace, name) {
break;
}
self.pop();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FormattingEntry {
Marker,
Element(NodeId),
}
#[derive(Debug, Default)]
pub(crate) struct ActiveFormattingElements {
entries: Vec<FormattingEntry>,
}
impl ActiveFormattingElements {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push_marker(&mut self) {
self.entries.push(FormattingEntry::Marker);
}
pub(crate) fn push(&mut self, document: &Document, element: NodeId) {
let boundary = self
.entries
.iter()
.rposition(|entry| matches!(entry, FormattingEntry::Marker))
.map_or(0, |marker_index| marker_index + 1);
let matching: Vec<usize> = self.entries[boundary..]
.iter()
.enumerate()
.filter_map(|(offset, entry)| match entry {
FormattingEntry::Element(node)
if elements_match_for_noahs_ark(document, *node, element) =>
{
Some(boundary + offset)
}
_ => None,
})
.collect();
if matching.len() >= 3 {
self.entries.remove(matching[0]);
}
self.entries.push(FormattingEntry::Element(element));
}
pub(crate) fn clear_up_to_last_marker(&mut self) {
while let Some(entry) = self.entries.pop() {
if matches!(entry, FormattingEntry::Marker) {
break;
}
}
}
}
fn elements_match_for_noahs_ark(document: &Document, a: NodeId, b: NodeId) -> bool {
let NodeKind::Element {
name: name_a,
namespace: namespace_a,
attributes: attributes_a,
} = &document.node(a).kind
else {
return false;
};
let NodeKind::Element {
name: name_b,
namespace: namespace_b,
attributes: attributes_b,
} = &document.node(b).kind
else {
return false;
};
if name_a != name_b || namespace_a != namespace_b || attributes_a.len() != attributes_b.len() {
return false;
}
let mut sorted_a: Vec<_> = attributes_a
.iter()
.map(|attribute| (&attribute.name, &attribute.namespace, &attribute.value))
.collect();
let mut sorted_b: Vec<_> = attributes_b
.iter()
.map(|attribute| (&attribute.name, &attribute.namespace, &attribute.value))
.collect();
sorted_a.sort();
sorted_b.sort();
sorted_a == sorted_b
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum QuirksMode {
#[default]
NoQuirks,
Quirks,
LimitedQuirks,
}
const QUIRKS_PUBLIC_IDENTIFIER_EXACT: &[&str] = &[
"-//W3O//DTD W3 HTML Strict 3.0//EN//",
"-/W3C/DTD HTML 4.0 Transitional/EN",
"HTML",
];
const QUIRKS_SYSTEM_IDENTIFIER_EXACT: &str =
"http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd";
const QUIRKS_PUBLIC_IDENTIFIER_PREFIXES: &[&str] = &[
"+//Silmaril//dtd html Pro v0r11 19970101//",
"-//AS//DTD HTML 3.0 asWedit + extensions//",
"-//AdvaSoft Ltd//DTD HTML 3.0 asWedit + extensions//",
"-//IETF//DTD HTML 2.0 Level 1//",
"-//IETF//DTD HTML 2.0 Level 2//",
"-//IETF//DTD HTML 2.0 Strict Level 1//",
"-//IETF//DTD HTML 2.0 Strict Level 2//",
"-//IETF//DTD HTML 2.0 Strict//",
"-//IETF//DTD HTML 2.0//",
"-//IETF//DTD HTML 2.1E//",
"-//IETF//DTD HTML 3.0//",
"-//IETF//DTD HTML 3.2 Final//",
"-//IETF//DTD HTML 3.2//",
"-//IETF//DTD HTML 3//",
"-//IETF//DTD HTML Level 0//",
"-//IETF//DTD HTML Level 1//",
"-//IETF//DTD HTML Level 2//",
"-//IETF//DTD HTML Level 3//",
"-//IETF//DTD HTML Strict Level 0//",
"-//IETF//DTD HTML Strict Level 1//",
"-//IETF//DTD HTML Strict Level 2//",
"-//IETF//DTD HTML Strict Level 3//",
"-//IETF//DTD HTML Strict//",
"-//IETF//DTD HTML//",
"-//Metrius//DTD Metrius Presentational//",
"-//Microsoft//DTD Internet Explorer 2.0 HTML Strict//",
"-//Microsoft//DTD Internet Explorer 2.0 HTML//",
"-//Microsoft//DTD Internet Explorer 2.0 Tables//",
"-//Microsoft//DTD Internet Explorer 3.0 HTML Strict//",
"-//Microsoft//DTD Internet Explorer 3.0 HTML//",
"-//Microsoft//DTD Internet Explorer 3.0 Tables//",
"-//Netscape Comm. Corp.//DTD HTML//",
"-//Netscape Comm. Corp.//DTD Strict HTML//",
"-//O'Reilly and Associates//DTD HTML 2.0//",
"-//O'Reilly and Associates//DTD HTML Extended 1.0//",
"-//O'Reilly and Associates//DTD HTML Extended Relaxed 1.0//",
"-//SQ//DTD HTML 2.0 HoTMetaL + extensions//",
"-//SoftQuad Software//DTD HoTMetaL PRO 6.0::19990601::extensions to HTML 4.0//",
"-//SoftQuad//DTD HoTMetaL PRO 4.0::19971010::extensions to HTML 4.0//",
"-//Spyglass//DTD HTML 2.0 Extended//",
"-//Sun Microsystems Corp.//DTD HotJava HTML//",
"-//Sun Microsystems Corp.//DTD HotJava Strict HTML//",
"-//W3C//DTD HTML 3 1995-03-24//",
"-//W3C//DTD HTML 3.2 Draft//",
"-//W3C//DTD HTML 3.2 Final//",
"-//W3C//DTD HTML 3.2//",
"-//W3C//DTD HTML 3.2S Draft//",
"-//W3C//DTD HTML 4.0 Frameset//",
"-//W3C//DTD HTML 4.0 Transitional//",
"-//W3C//DTD HTML Experimental 19960712//",
"-//W3C//DTD HTML Experimental 970421//",
"-//W3C//DTD W3 HTML//",
"-//W3O//DTD W3 HTML 3.0//",
"-//WebTechs//DTD Mozilla HTML 2.0//",
"-//WebTechs//DTD Mozilla HTML//",
];
const QUIRKS_PUBLIC_IDENTIFIER_PREFIXES_WITHOUT_SYSTEM_IDENTIFIER: &[&str] = &[
"-//W3C//DTD HTML 4.01 Frameset//",
"-//W3C//DTD HTML 4.01 Transitional//",
];
const LIMITED_QUIRKS_PUBLIC_IDENTIFIER_PREFIXES: &[&str] = &[
"-//W3C//DTD XHTML 1.0 Frameset//",
"-//W3C//DTD XHTML 1.0 Transitional//",
];
const LIMITED_QUIRKS_PUBLIC_IDENTIFIER_PREFIXES_WITH_SYSTEM_IDENTIFIER: &[&str] = &[
"-//W3C//DTD HTML 4.01 Frameset//",
"-//W3C//DTD HTML 4.01 Transitional//",
];
fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
}
pub(crate) fn determine_quirks_mode(doctype: &DoctypeToken) -> QuirksMode {
let name = doctype.name.as_deref();
let public_id = doctype.public_identifier.as_deref();
let system_id = doctype.system_identifier.as_deref();
let system_id_missing_or_empty = system_id.is_none_or(str::is_empty);
let quirks = doctype.force_quirks
|| name != Some("html")
|| public_id.is_some_and(|id| {
QUIRKS_PUBLIC_IDENTIFIER_EXACT
.iter()
.any(|exact| id.eq_ignore_ascii_case(exact))
})
|| system_id.is_some_and(|id| id.eq_ignore_ascii_case(QUIRKS_SYSTEM_IDENTIFIER_EXACT))
|| public_id.is_some_and(|id| {
QUIRKS_PUBLIC_IDENTIFIER_PREFIXES
.iter()
.any(|prefix| starts_with_ignore_ascii_case(id, prefix))
})
|| (system_id_missing_or_empty
&& public_id.is_some_and(|id| {
QUIRKS_PUBLIC_IDENTIFIER_PREFIXES_WITHOUT_SYSTEM_IDENTIFIER
.iter()
.any(|prefix| starts_with_ignore_ascii_case(id, prefix))
}));
if quirks {
return QuirksMode::Quirks;
}
let limited_quirks = public_id.is_some_and(|id| {
LIMITED_QUIRKS_PUBLIC_IDENTIFIER_PREFIXES
.iter()
.any(|prefix| starts_with_ignore_ascii_case(id, prefix))
}) || (!system_id_missing_or_empty
&& public_id.is_some_and(|id| {
LIMITED_QUIRKS_PUBLIC_IDENTIFIER_PREFIXES_WITH_SYSTEM_IDENTIFIER
.iter()
.any(|prefix| starts_with_ignore_ascii_case(id, prefix))
}));
if limited_quirks {
QuirksMode::LimitedQuirks
} else {
QuirksMode::NoQuirks
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum InsertionMode {
#[default]
Initial,
BeforeHtml,
BeforeHead,
InHead,
InHeadNoscript,
AfterHead,
InBody,
Text,
InTable,
InTableText,
InCaption,
InColumnGroup,
InTableBody,
InRow,
InCell,
InTemplate,
AfterBody,
InFrameset,
AfterFrameset,
AfterAfterBody,
AfterAfterFrameset,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GenericTextElementKind {
RawText,
Rcdata,
Script,
}
enum TokenOutcome {
Consumed(Option<ExternalState>),
Reprocess,
}
fn is_whitespace(c: char) -> bool {
matches!(c, '\t' | '\n' | '\x0C' | '\r' | ' ')
}
struct InsertionLocation {
parent: NodeId,
before: Option<NodeId>,
}
pub(crate) struct TreeBuilder {
document: Document,
open_elements: OpenElementsStack,
active_formatting_elements: ActiveFormattingElements,
foster_parenting: bool,
insertion_mode: InsertionMode,
original_insertion_mode: Option<InsertionMode>,
quirks_mode: QuirksMode,
head_element_pointer: Option<NodeId>,
form_element_pointer: Option<NodeId>,
frameset_ok: bool,
skip_next_line_feed: bool,
pending_table_character_tokens: Vec<(char, Position)>,
stack_of_template_insertion_modes: Vec<InsertionMode>,
}
impl TreeBuilder {
pub(crate) fn new() -> Self {
TreeBuilder {
document: Document::new(),
open_elements: OpenElementsStack::new(),
active_formatting_elements: ActiveFormattingElements::new(),
foster_parenting: false,
insertion_mode: InsertionMode::default(),
original_insertion_mode: None,
quirks_mode: QuirksMode::default(),
head_element_pointer: None,
form_element_pointer: None,
frameset_ok: true,
skip_next_line_feed: false,
pending_table_character_tokens: Vec::new(),
stack_of_template_insertion_modes: Vec::new(),
}
}
fn appropriate_place_for_inserting_a_node(
&self,
override_target: Option<NodeId>,
) -> InsertionLocation {
let target = override_target.unwrap_or_else(|| {
self.open_elements
.current_node()
.expect("inserting a node requires a current node on the stack")
});
let location = if self.foster_parenting && self.is_foster_parenting_target(target) {
self.foster_parenting_location()
} else {
InsertionLocation {
parent: target,
before: None,
}
};
self.redirect_into_template_contents(location)
}
fn redirect_into_template_contents(&self, location: InsertionLocation) -> InsertionLocation {
if !self.node_has_html_name(location.parent, "template") {
return location;
}
InsertionLocation {
parent: self.template_contents(location.parent),
before: None,
}
}
fn is_foster_parenting_target(&self, node: NodeId) -> bool {
let NodeKind::Element {
name, namespace, ..
} = &self.document.node(node).kind
else {
return false;
};
namespace.as_deref() == Some(HTML_NAMESPACE)
&& matches!(name.as_str(), "table" | "tbody" | "tfoot" | "thead" | "tr")
}
fn foster_parenting_location(&self) -> InsertionLocation {
let last_template =
self.open_elements
.last_matching(&self.document, HTML_NAMESPACE, "template");
let last_table = self
.open_elements
.last_matching(&self.document, HTML_NAMESPACE, "table");
if let Some(last_template) = last_template {
let use_template = match last_table {
None => true,
Some(last_table) => self.open_elements.is_lower(last_template, last_table),
};
if use_template {
return InsertionLocation {
parent: last_template,
before: None,
};
}
}
let Some(last_table) = last_table else {
let html = self
.open_elements
.topmost()
.expect("foster parenting requires at least the html element on the stack");
return InsertionLocation {
parent: html,
before: None,
};
};
if let Some(parent) = self.document.parent(last_table) {
return InsertionLocation {
parent,
before: Some(last_table),
};
}
let previous_element = self
.open_elements
.element_immediately_above(last_table)
.expect("a table with no parent must still have something above it on the stack");
InsertionLocation {
parent: previous_element,
before: None,
}
}
fn create_element_for_token(
&mut self,
tag: &TagToken,
namespace: &str,
position: Option<Position>,
) -> NodeId {
let attributes = if namespace == HTML_NAMESPACE {
tag.attributes
.iter()
.cloned()
.map(Attribute::from)
.collect()
} else {
adjust_attributes_for_foreign_element(tag, namespace)
};
let element = self.document.new_node(
NodeKind::Element {
name: tag.name.clone(),
namespace: Some(namespace.to_owned()),
attributes,
},
position,
);
if namespace == HTML_NAMESPACE && tag.name == "template" {
let content = self.document.new_node(NodeKind::DocumentFragment, None);
self.document.append_child(element, content);
}
element
}
fn template_contents(&self, template: NodeId) -> NodeId {
self.document.children(template).next().expect(
"a template element always has its template-contents fragment as its first (and \
only, until real content is inserted into it) tree child",
)
}
fn insert_element_at_adjusted_insertion_location(
&mut self,
element: NodeId,
location: InsertionLocation,
) {
if location.parent == self.document.root()
&& self.document.children(location.parent).next().is_some()
{
return;
}
self.document
.insert_before(location.parent, location.before, element);
}
fn insert_foreign_element(
&mut self,
tag: &TagToken,
namespace: &str,
only_add_to_element_stack: bool,
position: Option<Position>,
) -> NodeId {
let location = self.appropriate_place_for_inserting_a_node(None);
let element = self.create_element_for_token(tag, namespace, position);
if !only_add_to_element_stack {
self.insert_element_at_adjusted_insertion_location(element, location);
}
self.open_elements.push(element);
element
}
fn insert_html_element(&mut self, tag: &TagToken, position: Option<Position>) -> NodeId {
self.insert_foreign_element(tag, HTML_NAMESPACE, false, position)
}
fn generic_text_element_parsing_algorithm(
&mut self,
tag: &TagToken,
kind: GenericTextElementKind,
position: Option<Position>,
) -> (NodeId, ExternalState) {
let element = self.insert_html_element(tag, position);
let external_state = match kind {
GenericTextElementKind::RawText => ExternalState::RawText,
GenericTextElementKind::Rcdata => ExternalState::RcData,
GenericTextElementKind::Script => ExternalState::ScriptData,
};
self.original_insertion_mode = Some(self.insertion_mode);
self.insertion_mode = InsertionMode::Text;
(element, external_state)
}
fn tag_token_for(&self, node: NodeId) -> TagToken {
let NodeKind::Element {
name, attributes, ..
} = &self.document.node(node).kind
else {
panic!("tag_token_for called on a non-element node")
};
TagToken {
name: name.clone(),
self_closing: false,
attributes: attributes
.iter()
.map(|attribute| crate::tokenizer::Attribute {
name: attribute.name.clone(),
value: attribute.value.clone(),
})
.collect(),
}
}
fn reconstruct_the_active_formatting_elements(&mut self) {
let Some(&last) = self.active_formatting_elements.entries.last() else {
return;
};
let is_already_active = match last {
FormattingEntry::Marker => true,
FormattingEntry::Element(node) => self.open_elements.contains(node),
};
if is_already_active {
return;
}
let last_index = self.active_formatting_elements.entries.len() - 1;
let mut index = last_index;
let create_index = loop {
if index == 0 {
break 0;
}
index -= 1;
let is_stop = match self.active_formatting_elements.entries[index] {
FormattingEntry::Marker => true,
FormattingEntry::Element(node) => self.open_elements.contains(node),
};
if is_stop {
break index + 1;
}
};
let mut index = create_index;
loop {
let FormattingEntry::Element(old_node) = self.active_formatting_elements.entries[index]
else {
unreachable!(
"rewinding stops at a marker or in-stack element, so the Create step's \
entry (one position later) is always a formatting element"
)
};
let tag = self.tag_token_for(old_node);
let new_node = self.insert_html_element(&tag, None);
self.active_formatting_elements.entries[index] = FormattingEntry::Element(new_node);
if index == last_index {
break;
}
index += 1;
}
}
fn any_other_end_tag_in_body(&mut self, tag: &TagToken) {
let mut node = self
.open_elements
.current_node()
.expect("any other end tag requires at least one open element");
loop {
let is_match = matches!(
&self.document.node(node).kind,
NodeKind::Element { name, namespace, .. }
if namespace.as_deref() == Some(HTML_NAMESPACE) && name == &tag.name
);
if is_match {
self.with_option_pop_hook(|this| {
this.open_elements
.generate_implied_end_tags(&this.document, Some(tag.name.as_str()));
loop {
let popped = this.open_elements.pop();
if popped == Some(node) {
break;
}
}
});
return;
}
if is_special(&self.document, node) {
return;
}
node = self.open_elements.element_immediately_above(node).expect(
"the html element is always in SPECIAL_CATEGORY, so this loop always \
returns via the is_special check above before running out of stack \
entries to walk above",
);
}
}
fn adoption_agency_algorithm(&mut self, tag: &TagToken) {
let subject = tag.name.as_str();
if let Some(current) = self.open_elements.current_node() {
let is_subject = matches!(
&self.document.node(current).kind,
NodeKind::Element { name, namespace, .. }
if namespace.as_deref() == Some(HTML_NAMESPACE) && name == subject
);
let is_active_formatting_element =
self.active_formatting_elements.entries.iter().any(
|entry| matches!(entry, FormattingEntry::Element(node) if *node == current),
);
if is_subject && !is_active_formatting_element {
self.open_elements.pop();
return;
}
}
for _ in 0..8 {
let marker_boundary = self
.active_formatting_elements
.entries
.iter()
.rposition(|entry| matches!(entry, FormattingEntry::Marker))
.map_or(0, |marker_index| marker_index + 1);
let formatting_element = self.active_formatting_elements.entries[marker_boundary..]
.iter()
.rev()
.find_map(|entry| match entry {
FormattingEntry::Element(node) => {
let NodeKind::Element { name, .. } = &self.document.node(*node).kind else {
unreachable!("active formatting elements are always elements")
};
(name == subject).then_some(*node)
}
FormattingEntry::Marker => None,
});
let Some(formatting_element) = formatting_element else {
self.any_other_end_tag_in_body(tag);
return;
};
if !self.open_elements.contains(formatting_element) {
let index = self
.active_formatting_elements
.entries
.iter()
.position(|entry| {
matches!(entry, FormattingEntry::Element(node) if *node == formatting_element)
})
.expect("formattingElement was just found in this list");
self.active_formatting_elements.entries.remove(index);
return;
}
let NodeKind::Element {
name: formatting_element_name,
..
} = &self.document.node(formatting_element).kind
else {
unreachable!("formattingElement is always an element")
};
if !self
.open_elements
.has_element_in_scope(&self.document, formatting_element_name)
{
return;
}
let furthest_block = self
.open_elements
.topmost_special_element_below(&self.document, formatting_element);
let Some(furthest_block) = furthest_block else {
loop {
let popped = self.open_elements.pop();
if popped == Some(formatting_element) {
break;
}
}
let index = self
.active_formatting_elements
.entries
.iter()
.position(|entry| {
matches!(entry, FormattingEntry::Element(node) if *node == formatting_element)
})
.expect("formattingElement was just found in this list");
self.active_formatting_elements.entries.remove(index);
return;
};
let common_ancestor = self
.open_elements
.element_immediately_above(formatting_element)
.expect("formattingElement always has an element above it (at minimum html)");
let mut bookmark = self
.active_formatting_elements
.entries
.iter()
.position(|entry| {
matches!(entry, FormattingEntry::Element(node) if *node == formatting_element)
})
.expect("formattingElement was just found in this list");
let mut node;
let mut last_node = furthest_block;
let mut node_above = self.open_elements.element_immediately_above(furthest_block);
let mut inner_loop_counter = 0u32;
loop {
inner_loop_counter += 1;
node = node_above.expect(
"walking upward from furthestBlock always reaches formattingElement, \
which sits strictly above it, before running out of stack entries",
);
node_above = self.open_elements.element_immediately_above(node);
if node == formatting_element {
break;
}
if inner_loop_counter > 3
&& let Some(index) = self.active_formatting_elements.entries.iter().position(
|entry| matches!(entry, FormattingEntry::Element(afe_node) if *afe_node == node),
)
{
self.active_formatting_elements.entries.remove(index);
if index < bookmark {
bookmark -= 1;
}
}
let node_afe_index = self.active_formatting_elements.entries.iter().position(
|entry| matches!(entry, FormattingEntry::Element(afe_node) if *afe_node == node),
);
let Some(node_afe_index) = node_afe_index else {
let stack_index = self
.open_elements
.entries
.iter()
.position(|&entry| entry == node)
.expect("node is currently on the stack of open elements");
self.open_elements.entries.remove(stack_index);
continue;
};
let node_tag = self.tag_token_for(node);
let new_node = self.create_element_for_token(&node_tag, HTML_NAMESPACE, None);
self.active_formatting_elements.entries[node_afe_index] =
FormattingEntry::Element(new_node);
let stack_index = self
.open_elements
.entries
.iter()
.position(|&entry| entry == node)
.expect("node is currently on the stack of open elements");
self.open_elements.entries[stack_index] = new_node;
if last_node == furthest_block {
bookmark = node_afe_index + 1;
}
self.document.append_child(new_node, last_node);
last_node = new_node;
}
let location = self.appropriate_place_for_inserting_a_node(Some(common_ancestor));
self.document.remove(last_node);
let last_node_parent_is_null = self.document.parent(last_node).is_none();
let last_node_is_not_ancestor_of_target = !self
.document
.is_inclusive_ancestor(last_node, location.parent);
let target_is_not_document_with_element_child =
!(matches!(self.document.node(location.parent).kind, NodeKind::Document)
&& self.document.children(location.parent).any(|child| {
matches!(self.document.node(child).kind, NodeKind::Element { .. })
}));
let ref_node_is_null_or_its_parent_is_target = match location.before {
Some(reference) => self.document.parent(reference) == Some(location.parent),
None => true,
};
if last_node_parent_is_null
&& last_node_is_not_ancestor_of_target
&& target_is_not_document_with_element_child
&& ref_node_is_null_or_its_parent_is_target
{
self.document
.insert_before(location.parent, location.before, last_node);
}
let formatting_element_tag = self.tag_token_for(formatting_element);
let new_formatting_element =
self.create_element_for_token(&formatting_element_tag, HTML_NAMESPACE, None);
let furthest_block_children: Vec<_> = self.document.children(furthest_block).collect();
for child in furthest_block_children {
self.document.append_child(new_formatting_element, child);
}
self.document
.append_child(furthest_block, new_formatting_element);
let formatting_element_afe_index = self
.active_formatting_elements
.entries
.iter()
.position(|entry| {
matches!(entry, FormattingEntry::Element(node) if *node == formatting_element)
})
.expect("formattingElement's own entry is never touched by the inner loop");
self.active_formatting_elements
.entries
.remove(formatting_element_afe_index);
if formatting_element_afe_index < bookmark {
bookmark -= 1;
}
self.active_formatting_elements
.entries
.insert(bookmark, FormattingEntry::Element(new_formatting_element));
let formatting_element_stack_index = self
.open_elements
.entries
.iter()
.position(|&entry| entry == formatting_element)
.expect("formattingElement is still on the stack of open elements");
self.open_elements
.entries
.remove(formatting_element_stack_index);
let furthest_block_stack_index = self
.open_elements
.entries
.iter()
.position(|&entry| entry == furthest_block)
.expect("furthestBlock is still on the stack of open elements");
self.open_elements
.entries
.insert(furthest_block_stack_index + 1, new_formatting_element);
}
}
fn insert_character(&mut self, c: char, position: Option<Position>) {
let location = self.appropriate_place_for_inserting_a_node(None);
if location.parent == self.document.root() {
return;
}
let node_immediately_before = match location.before {
Some(before) => self.document.prev_sibling(before),
None => self.document.last_child(location.parent),
};
if let Some(node) = node_immediately_before
&& let NodeKind::Text { content } = &mut self.document.node_mut(node).kind
{
content.push(c);
return;
}
let text = self.document.new_node(
NodeKind::Text {
content: c.to_string(),
},
position,
);
self.document
.insert_before(location.parent, location.before, text);
}
fn insert_comment(&mut self, data: String, position: Option<Position>) {
let location = self.appropriate_place_for_inserting_a_node(None);
let comment = self
.document
.new_node(NodeKind::Comment { content: data }, position);
self.document
.insert_before(location.parent, location.before, comment);
}
fn insert_processing_instruction(
&mut self,
target: String,
data: String,
position: Option<Position>,
) {
let location = self.appropriate_place_for_inserting_a_node(None);
let pi = self
.document
.new_node(NodeKind::ProcessingInstruction { target, data }, position);
self.document
.insert_before(location.parent, location.before, pi);
}
fn append_comment_to_document(&mut self, data: String, position: Position) {
let comment = self
.document
.new_node(NodeKind::Comment { content: data }, Some(position));
let root = self.document.root();
self.document.append_child(root, comment);
}
fn append_processing_instruction_to_document(
&mut self,
target: String,
data: String,
position: Position,
) {
let pi = self.document.new_node(
NodeKind::ProcessingInstruction { target, data },
Some(position),
);
let root = self.document.root();
self.document.append_child(root, pi);
}
fn append_comment_to_html_element(&mut self, data: String, position: Position) {
let html = self
.open_elements
.topmost()
.expect("\"after body\" always runs with html on the stack of open elements");
let comment = self
.document
.new_node(NodeKind::Comment { content: data }, Some(position));
self.document.append_child(html, comment);
}
fn append_processing_instruction_to_html_element(
&mut self,
target: String,
data: String,
position: Position,
) {
let html = self
.open_elements
.topmost()
.expect("\"after body\" always runs with html on the stack of open elements");
let pi = self.document.new_node(
NodeKind::ProcessingInstruction { target, data },
Some(position),
);
self.document.append_child(html, pi);
}
fn merge_attributes_onto_html_element(&mut self, tag: &TagToken) {
if self.has_template_on_stack() {
return;
}
let html = self
.open_elements
.topmost()
.expect("this rule never runs before the html element has been pushed");
self.merge_new_attributes_onto(html, tag);
}
fn merge_new_attributes_onto(&mut self, node: NodeId, tag: &TagToken) {
let NodeKind::Element { attributes, .. } = &mut self.document.node_mut(node).kind else {
unreachable!("merge_new_attributes_onto is always called with an element node")
};
for attribute in &tag.attributes {
if !attributes
.iter()
.any(|existing| existing.name == attribute.name)
{
attributes.push(Attribute::from(attribute.clone()));
}
}
}
pub(crate) fn process_token(
&mut self,
kind: &TokenKind,
position: Position,
) -> Option<ExternalState> {
loop {
let outcome = if self.should_process_as_foreign_content(kind) {
self.process_token_foreign_content(kind, position)
} else {
self.process_token_by_insertion_mode(kind, position)
};
match outcome {
TokenOutcome::Consumed(state) => return state,
TokenOutcome::Reprocess => continue,
}
}
}
fn process_token_by_insertion_mode(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match self.insertion_mode {
InsertionMode::Initial => self.process_token_initial(kind, position),
InsertionMode::BeforeHtml => self.process_token_before_html(kind, position),
InsertionMode::BeforeHead => self.process_token_before_head(kind, position),
InsertionMode::InHead => self.process_token_in_head(kind, position),
InsertionMode::InHeadNoscript => self.process_token_in_head_noscript(kind, position),
InsertionMode::AfterHead => self.process_token_after_head(kind, position),
InsertionMode::InBody => self.process_token_in_body(kind, position),
InsertionMode::Text => self.process_token_text(kind, position),
InsertionMode::InTable => self.process_token_in_table(kind, position),
InsertionMode::InTableText => self.process_token_in_table_text(kind, position),
InsertionMode::InCaption => self.process_token_in_caption(kind, position),
InsertionMode::InColumnGroup => self.process_token_in_column_group(kind, position),
InsertionMode::InTableBody => self.process_token_in_table_body(kind, position),
InsertionMode::InRow => self.process_token_in_row(kind, position),
InsertionMode::InCell => self.process_token_in_cell(kind, position),
InsertionMode::InTemplate => self.process_token_in_template(kind, position),
InsertionMode::AfterBody => self.process_token_after_body(kind, position),
InsertionMode::InFrameset => self.process_token_in_frameset(kind, position),
InsertionMode::AfterFrameset => self.process_token_after_frameset(kind, position),
InsertionMode::AfterAfterBody => self.process_token_after_after_body(kind, position),
InsertionMode::AfterAfterFrameset => {
self.process_token_after_after_frameset(kind, position)
}
}
}
fn should_process_as_foreign_content(&self, kind: &TokenKind) -> bool {
let Some(node) = self.adjusted_current_node() else {
return false;
};
if self.is_html_namespace_element(node) {
return false;
}
if is_mathml_text_integration_point(&self.document, node) {
match kind {
TokenKind::StartTag(tag)
if !matches!(tag.name.as_str(), "mglyph" | "malignmark") =>
{
return false;
}
TokenKind::Character(_) => return false,
_ => {}
}
}
if self.node_has_namespace_and_name(node, MATHML_NAMESPACE, "annotation-xml")
&& matches!(kind, TokenKind::StartTag(tag) if tag.name == "svg")
{
return false;
}
if is_html_integration_point(&self.document, node)
&& matches!(kind, TokenKind::StartTag(_) | TokenKind::Character(_))
{
return false;
}
if matches!(kind, TokenKind::Eof) {
return false;
}
true
}
fn pop_out_of_foreign_content(&mut self) {
while let Some(current) = self.open_elements.current_node() {
if is_mathml_text_integration_point(&self.document, current)
|| is_html_integration_point(&self.document, current)
|| self.is_html_namespace_element(current)
{
break;
}
self.open_elements.pop();
}
}
fn current_node_is_svg_script(&self) -> bool {
self.open_elements
.current_node()
.is_some_and(|node| self.node_has_namespace_and_name(node, SVG_NAMESPACE, "script"))
}
fn pop_until_node_popped(&mut self, target: NodeId) {
loop {
let popped = self.open_elements.pop();
if popped == Some(target) {
break;
}
}
}
fn process_token_foreign_content(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Character(c) if *c == '\0' => {
self.insert_character('\u{FFFD}', Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Character(c) => {
self.insert_character(*c, Some(position));
self.frameset_ok = false;
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"b" | "big"
| "blockquote"
| "body"
| "br"
| "center"
| "code"
| "dd"
| "div"
| "dl"
| "dt"
| "em"
| "embed"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "head"
| "hr"
| "i"
| "img"
| "li"
| "listing"
| "menu"
| "meta"
| "nobr"
| "ol"
| "p"
| "pre"
| "ruby"
| "s"
| "small"
| "span"
| "strong"
| "strike"
| "sub"
| "sup"
| "table"
| "tt"
| "u"
| "ul"
| "var"
) || (tag.name == "font"
&& tag.attributes.iter().any(|attribute| {
matches!(attribute.name.as_str(), "color" | "face" | "size")
})) =>
{
self.pop_out_of_foreign_content();
TokenOutcome::Reprocess
}
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "br" | "p") => {
self.pop_out_of_foreign_content();
TokenOutcome::Reprocess
}
TokenKind::StartTag(tag) => {
let namespace = self
.adjusted_current_node()
.and_then(|node| self.namespace_of(node))
.expect("foreign content dispatch always has a foreign adjusted current node");
let adjusted_tag = if namespace == SVG_NAMESPACE {
TagToken {
name: adjust_svg_tag_name(&tag.name).to_owned(),
..tag.clone()
}
} else {
tag.clone()
};
self.insert_foreign_element(&adjusted_tag, &namespace, false, Some(position));
if adjusted_tag.self_closing {
self.open_elements.pop();
}
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "script" && self.current_node_is_svg_script() => {
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) => {
let mut node = self.open_elements.current_node();
loop {
let current = node.expect(
"the stack of open elements always contains at least html by the time \
this runs",
);
if self.open_elements.topmost() == Some(current) {
return TokenOutcome::Consumed(None);
}
if self.element_name_matches_ignore_case(current, &tag.name) {
self.pop_until_node_popped(current);
return TokenOutcome::Consumed(None);
}
let next = self
.open_elements
.element_immediately_above(current)
.expect("current is not topmost, so an element always sits above it");
if self.is_html_namespace_element(next) {
return self.process_token_by_insertion_mode(kind, position);
}
node = Some(next);
}
}
TokenKind::Eof => unreachable!(
"should_process_as_foreign_content always returns false for an EOF token, \
so process_token never dispatches one here"
),
}
}
fn element_name_matches_ignore_case(&self, node: NodeId, tag_name: &str) -> bool {
matches!(
&self.document.node(node).kind,
NodeKind::Element { name, .. } if name.eq_ignore_ascii_case(tag_name)
)
}
fn process_token_initial(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => TokenOutcome::Consumed(None),
TokenKind::Comment(data) => {
self.append_comment_to_document(data.clone(), position);
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.append_processing_instruction_to_document(
pi.target.clone(),
pi.data.clone(),
position,
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(doctype) => {
let root = self.document.root();
let has_doctype_or_element_child = self.document.children(root).any(|child| {
matches!(
self.document.node(child).kind,
NodeKind::Doctype { .. } | NodeKind::Element { .. }
)
});
if !has_doctype_or_element_child {
let doctype_node = self.document.new_node(
NodeKind::Doctype {
name: Some(doctype.name.clone().unwrap_or_default()),
public_identifier: Some(
doctype.public_identifier.clone().unwrap_or_default(),
),
system_identifier: Some(
doctype.system_identifier.clone().unwrap_or_default(),
),
},
Some(position),
);
self.document.append_child(root, doctype_node);
}
self.quirks_mode = determine_quirks_mode(doctype);
self.insertion_mode = InsertionMode::BeforeHtml;
TokenOutcome::Consumed(None)
}
_ => {
self.quirks_mode = QuirksMode::Quirks;
self.insertion_mode = InsertionMode::BeforeHtml;
TokenOutcome::Reprocess
}
}
}
fn process_token_before_html(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::Comment(data) => {
self.append_comment_to_document(data.clone(), position);
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.append_processing_instruction_to_document(
pi.target.clone(),
pi.data.clone(),
position,
);
TokenOutcome::Consumed(None)
}
TokenKind::Character(c) if is_whitespace(*c) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
let element = self.create_element_for_token(tag, HTML_NAMESPACE, Some(position));
let root = self.document.root();
self.document.append_child(root, element);
self.open_elements.push(element);
self.insertion_mode = InsertionMode::BeforeHead;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(tag.name.as_str(), "head" | "body" | "html" | "br") =>
{
self.before_html_anything_else()
}
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
_ => self.before_html_anything_else(),
}
}
fn before_html_anything_else(&mut self) -> TokenOutcome {
let html = self.document.new_node(
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
let root = self.document.root();
self.document.append_child(root, html);
self.open_elements.push(html);
self.insertion_mode = InsertionMode::BeforeHead;
TokenOutcome::Reprocess
}
fn process_token_before_head(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => TokenOutcome::Consumed(None),
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.merge_attributes_onto_html_element(tag);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "head" => {
let head = self.insert_html_element(tag, Some(position));
self.head_element_pointer = Some(head);
self.insertion_mode = InsertionMode::InHead;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(tag.name.as_str(), "head" | "body" | "html" | "br") =>
{
self.before_head_anything_else()
}
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
_ => self.before_head_anything_else(),
}
}
fn before_head_anything_else(&mut self) -> TokenOutcome {
let head_tag = TagToken {
name: "head".to_owned(),
self_closing: false,
attributes: vec![],
};
let head = self.insert_html_element(&head_tag, None);
self.head_element_pointer = Some(head);
self.insertion_mode = InsertionMode::InHead;
TokenOutcome::Reprocess
}
fn node_has_html_name(&self, node: NodeId, name: &str) -> bool {
self.node_has_namespace_and_name(node, HTML_NAMESPACE, name)
}
fn node_has_namespace_and_name(&self, node: NodeId, namespace: &str, name: &str) -> bool {
matches!(
&self.document.node(node).kind,
NodeKind::Element { name: node_name, namespace: node_namespace, .. }
if node_namespace.as_deref() == Some(namespace) && node_name == name
)
}
fn node_has_html_attribute(&self, node: NodeId, attribute_name: &str) -> bool {
matches!(
&self.document.node(node).kind,
NodeKind::Element { namespace, attributes, .. }
if namespace.as_deref() == Some(HTML_NAMESPACE)
&& attributes.iter().any(|attribute| attribute.name == attribute_name)
)
}
fn nearest_ancestor_select(&self, node: NodeId) -> Option<NodeId> {
let mut ancestor_optgroup = None;
let mut current = self.document.parent(node);
while let Some(ancestor) = current {
if self.node_has_html_name(ancestor, "datalist")
|| self.node_has_html_name(ancestor, "hr")
|| self.node_has_html_name(ancestor, "option")
{
return None;
}
if self.node_has_html_name(ancestor, "optgroup") {
if ancestor_optgroup.is_some() {
return None;
}
ancestor_optgroup = Some(ancestor);
}
if self.node_has_html_name(ancestor, "select") {
return Some(ancestor);
}
current = self.document.parent(ancestor);
}
None
}
fn list_of_options(&self, select: NodeId) -> Vec<NodeId> {
let mut options = Vec::new();
for child in self.document.children(select) {
if self.node_has_html_name(child, "option") {
options.push(child);
} else if self.node_has_html_name(child, "optgroup") {
options.extend(
self.document
.children(child)
.filter(|&grandchild| self.node_has_html_name(grandchild, "option")),
);
}
}
options
}
fn selected_option_of(&self, select: NodeId) -> Option<NodeId> {
let options = self.list_of_options(select);
options
.iter()
.rev()
.find(|&&option| self.node_has_html_attribute(option, "selected"))
.copied()
.or_else(|| {
options
.into_iter()
.find(|&option| !self.node_has_html_attribute(option, "disabled"))
})
}
fn first_selectedcontent_descendant(&self, select: NodeId) -> Option<NodeId> {
if self.node_has_html_attribute(select, "multiple") {
return None;
}
self.first_descendant_matching(select, "selectedcontent")
}
fn first_descendant_matching(&self, node: NodeId, name: &str) -> Option<NodeId> {
for child in self.document.children(node) {
if self.node_has_html_name(child, name) {
return Some(child);
}
if let Some(found) = self.first_descendant_matching(child, name) {
return Some(found);
}
}
None
}
fn maybe_clone_option_into_selectedcontent(&mut self, option: NodeId) {
let Some(select) = self.nearest_ancestor_select(option) else {
return;
};
if self.selected_option_of(select) != Some(option) {
return;
}
let Some(selectedcontent) = self.first_selectedcontent_descendant(select) else {
return;
};
let existing_children: Vec<_> = self.document.children(selectedcontent).collect();
for child in existing_children {
self.document.remove(child);
}
let option_children: Vec<_> = self.document.children(option).collect();
for child in option_children {
let clone = self.document.clone_subtree(child);
self.document.append_child(selectedcontent, clone);
}
}
fn with_option_pop_hook<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
let before: Vec<NodeId> = self.open_elements.entries.clone();
let result = f(self);
let after_len = self.open_elements.entries.len();
if after_len <= before.len() {
for &node in before[after_len..].iter().rev() {
if self.node_has_html_name(node, "option") {
self.maybe_clone_option_into_selectedcontent(node);
}
}
}
result
}
fn is_html_namespace_element(&self, node: NodeId) -> bool {
matches!(
&self.document.node(node).kind,
NodeKind::Element { namespace, .. } if namespace.as_deref() == Some(HTML_NAMESPACE)
)
}
fn namespace_of(&self, node: NodeId) -> Option<String> {
match &self.document.node(node).kind {
NodeKind::Element { namespace, .. } => namespace.clone(),
_ => None,
}
}
fn adjusted_current_node(&self) -> Option<NodeId> {
self.open_elements.current_node()
}
pub(crate) fn is_in_foreign_content(&self) -> bool {
self.adjusted_current_node()
.is_some_and(|node| !self.is_html_namespace_element(node))
}
pub(crate) fn stop_parsing(&mut self) {
self.with_option_pop_hook(|this| while this.open_elements.pop().is_some() {});
}
pub(crate) fn into_document(self) -> Document {
self.document
}
fn has_template_on_stack(&self) -> bool {
self.open_elements
.entries
.iter()
.any(|&node| self.node_has_html_name(node, "template"))
}
fn pop_until_one_of_popped(&mut self, names: &[&str]) {
loop {
let popped = self.open_elements.pop();
let is_match = popped.is_some_and(|node| {
names
.iter()
.any(|&name| self.node_has_html_name(node, name))
});
if is_match {
break;
}
}
}
fn remove_node_from_open_elements(&mut self, node: NodeId) {
if let Some(index) = self.open_elements.entries.iter().position(|&n| n == node) {
self.open_elements.entries.remove(index);
}
}
fn remove_node_from_active_formatting_elements(&mut self, node: NodeId) {
if let Some(index) = self.active_formatting_elements.entries.iter().position(
|entry| matches!(entry, FormattingEntry::Element(afe_node) if *afe_node == node),
) {
self.active_formatting_elements.entries.remove(index);
}
}
fn close_a_p_element(&mut self) {
self.open_elements
.generate_implied_end_tags(&self.document, Some("p"));
self.pop_until_one_of_popped(&["p"]);
}
fn has_node_in_scope(&self, node: NodeId) -> bool {
for &entry in self.open_elements.entries.iter().rev() {
if entry == node {
return true;
}
let NodeKind::Element {
name, namespace, ..
} = &self.document.node(entry).kind
else {
continue;
};
let namespace = namespace.as_deref().unwrap_or("");
if element_type_matches(DEFAULT_SCOPE, namespace, name) {
return false;
}
}
false
}
fn insert_void_element(&mut self, tag: &TagToken, position: Option<Position>) {
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, position);
self.open_elements.pop();
self.frameset_ok = false;
}
fn clear_stack_back_to_context(&mut self, names: &[&str]) {
while let Some(current) = self.open_elements.current_node() {
if names
.iter()
.any(|&name| self.node_has_html_name(current, name))
{
break;
}
self.open_elements.pop();
}
}
fn clear_stack_back_to_a_table_context(&mut self) {
self.clear_stack_back_to_context(&["table", "template", "html"]);
}
fn clear_stack_back_to_a_table_body_context(&mut self) {
self.clear_stack_back_to_context(&["tbody", "tfoot", "thead", "template", "html"]);
}
fn clear_stack_back_to_a_table_row_context(&mut self) {
self.clear_stack_back_to_context(&["tr", "template", "html"]);
}
fn reset_the_insertion_mode_appropriately(&mut self) {
let mut last = false;
let mut node = self.open_elements.current_node();
loop {
let current = node.expect(
"the stack of open elements always contains at least html by the time this runs",
);
if self.open_elements.topmost() == Some(current) {
last = true;
}
if !last
&& (self.node_has_html_name(current, "td")
|| self.node_has_html_name(current, "th"))
{
self.insertion_mode = InsertionMode::InCell;
return;
}
if self.node_has_html_name(current, "tr") {
self.insertion_mode = InsertionMode::InRow;
return;
}
if ["tbody", "thead", "tfoot"]
.iter()
.any(|&name| self.node_has_html_name(current, name))
{
self.insertion_mode = InsertionMode::InTableBody;
return;
}
if self.node_has_html_name(current, "caption") {
self.insertion_mode = InsertionMode::InCaption;
return;
}
if self.node_has_html_name(current, "colgroup") {
self.insertion_mode = InsertionMode::InColumnGroup;
return;
}
if self.node_has_html_name(current, "table") {
self.insertion_mode = InsertionMode::InTable;
return;
}
if self.node_has_html_name(current, "template") {
self.insertion_mode = *self
.stack_of_template_insertion_modes
.last()
.expect("a template element on the stack always has a matching entry here");
return;
}
if !last && self.node_has_html_name(current, "head") {
self.insertion_mode = InsertionMode::InHead;
return;
}
if self.node_has_html_name(current, "body") {
self.insertion_mode = InsertionMode::InBody;
return;
}
if self.node_has_html_name(current, "frameset") {
self.insertion_mode = InsertionMode::InFrameset;
return;
}
if self.node_has_html_name(current, "html") {
self.insertion_mode = if self.head_element_pointer.is_none() {
InsertionMode::BeforeHead
} else {
InsertionMode::AfterHead
};
return;
}
if last {
self.insertion_mode = InsertionMode::InBody;
return;
}
node = self.open_elements.element_immediately_above(current);
}
}
fn process_token_in_head(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.merge_attributes_onto_html_element(tag);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"base" | "basefont" | "bgsound" | "link" | "meta"
) =>
{
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "title" => {
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::Rcdata,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "noscript" => {
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InHeadNoscript;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "noframes" | "style") => {
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::RawText,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "script" => {
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::Script,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::EndTag(tag) if tag.name == "head" => {
self.open_elements.pop();
self.insertion_mode = InsertionMode::AfterHead;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "body" | "html" | "br") => {
self.in_head_anything_else()
}
TokenKind::StartTag(tag) if tag.name == "template" => {
self.active_formatting_elements.push_marker();
self.frameset_ok = false;
self.insertion_mode = InsertionMode::InTemplate;
self.stack_of_template_insertion_modes
.push(InsertionMode::InTemplate);
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "template" => {
if !self.has_template_on_stack() {
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags_thoroughly(&self.document);
self.pop_until_one_of_popped(&["template"]);
self.active_formatting_elements.clear_up_to_last_marker();
self.stack_of_template_insertion_modes.pop();
self.reset_the_insertion_mode_appropriately();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "head" => TokenOutcome::Consumed(None),
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
_ => self.in_head_anything_else(),
}
}
fn in_head_anything_else(&mut self) -> TokenOutcome {
self.open_elements.pop();
self.insertion_mode = InsertionMode::AfterHead;
TokenOutcome::Reprocess
}
fn process_token_in_head_noscript(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.merge_attributes_onto_html_element(tag);
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "noscript" => {
self.open_elements.pop();
self.insertion_mode = InsertionMode::InHead;
TokenOutcome::Consumed(None)
}
TokenKind::Character(c) if is_whitespace(*c) => {
self.process_token_in_head(kind, position)
}
TokenKind::Comment(_) | TokenKind::ProcessingInstruction(_) => {
self.process_token_in_head(kind, position)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"basefont" | "bgsound" | "link" | "meta" | "noframes" | "style"
) =>
{
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "br" => self.in_head_noscript_anything_else(),
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "head" | "noscript") => {
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
_ => self.in_head_noscript_anything_else(),
}
}
fn in_head_noscript_anything_else(&mut self) -> TokenOutcome {
self.open_elements.pop();
self.insertion_mode = InsertionMode::InHead;
TokenOutcome::Reprocess
}
fn process_token_after_head(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.merge_attributes_onto_html_element(tag);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "body" => {
self.insert_html_element(tag, Some(position));
self.frameset_ok = false;
self.insertion_mode = InsertionMode::InBody;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "frameset" => {
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InFrameset;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"base"
| "basefont"
| "bgsound"
| "link"
| "meta"
| "noframes"
| "script"
| "style"
| "template"
| "title"
) =>
{
let Some(head) = self.head_element_pointer else {
unreachable!(
"the head element pointer is always set by the time \"after head\" runs"
)
};
self.open_elements.push(head);
let outcome = self.process_token_in_head(kind, position);
let head_index = self
.open_elements
.entries
.iter()
.position(|&node| node == head);
if let Some(head_index) = head_index {
self.open_elements.entries.remove(head_index);
}
outcome
}
TokenKind::EndTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "body" | "html" | "br") => {
self.after_head_anything_else()
}
TokenKind::StartTag(tag) if tag.name == "head" => TokenOutcome::Consumed(None),
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
_ => self.after_head_anything_else(),
}
}
fn after_head_anything_else(&mut self) -> TokenOutcome {
let body_tag = TagToken {
name: "body".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_html_element(&body_tag, None);
self.insertion_mode = InsertionMode::InBody;
TokenOutcome::Reprocess
}
fn process_token_in_body(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) => {
if self.skip_next_line_feed {
self.skip_next_line_feed = false;
if *c == '\n' {
return TokenOutcome::Consumed(None);
}
}
if *c == '\0' {
return TokenOutcome::Consumed(None);
}
self.reconstruct_the_active_formatting_elements();
self.insert_character(*c, Some(position));
if !is_whitespace(*c) {
self.frameset_ok = false;
}
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.merge_attributes_onto_html_element(tag);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"base"
| "basefont"
| "bgsound"
| "link"
| "meta"
| "noframes"
| "script"
| "style"
| "template"
| "title"
) =>
{
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "body" => {
let second = self.open_elements.entries.get(1).copied();
let ignore = self.open_elements.entries.len() == 1
|| second.is_none_or(|node| !self.node_has_html_name(node, "body"))
|| self.has_template_on_stack();
if !ignore {
self.frameset_ok = false;
self.merge_new_attributes_onto(second.expect("checked by `ignore` above"), tag);
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "frameset" => {
let second = self.open_elements.entries.get(1).copied();
let ignore = self.open_elements.entries.len() == 1
|| second.is_none_or(|node| !self.node_has_html_name(node, "body"));
if ignore || !self.frameset_ok {
return TokenOutcome::Consumed(None);
}
self.document
.remove(second.expect("checked by `ignore` above"));
let html = self
.open_elements
.topmost()
.expect("the html element is always on the stack by the time \"in body\" runs");
while self.open_elements.current_node() != Some(html) {
self.open_elements.pop();
}
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InFrameset;
TokenOutcome::Consumed(None)
}
TokenKind::Eof => {
if !self.stack_of_template_insertion_modes.is_empty() {
return self.process_token_in_template(kind, position);
}
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "body" => {
if !self
.open_elements
.has_element_in_scope(&self.document, "body")
{
return TokenOutcome::Consumed(None);
}
self.insertion_mode = InsertionMode::AfterBody;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "html" => {
if !self
.open_elements
.has_element_in_scope(&self.document, "body")
{
return TokenOutcome::Consumed(None);
}
self.insertion_mode = InsertionMode::AfterBody;
TokenOutcome::Reprocess
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"address"
| "article"
| "aside"
| "blockquote"
| "center"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "header"
| "hgroup"
| "main"
| "menu"
| "nav"
| "ol"
| "p"
| "search"
| "section"
| "summary"
| "ul"
) =>
{
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(tag.name.as_str(), "h1" | "h2" | "h3" | "h4" | "h5" | "h6") =>
{
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
let current_is_heading = self.open_elements.current_node().is_some_and(|node| {
["h1", "h2", "h3", "h4", "h5", "h6"]
.iter()
.any(|&heading| self.node_has_html_name(node, heading))
});
if current_is_heading {
self.open_elements.pop();
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "pre" | "listing") => {
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
self.skip_next_line_feed = true;
self.frameset_ok = false;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "form" => {
if self.form_element_pointer.is_some() && !self.has_template_on_stack() {
return TokenOutcome::Consumed(None);
}
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
let form = self.insert_html_element(tag, Some(position));
if !self.has_template_on_stack() {
self.form_element_pointer = Some(form);
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "li" => {
self.frameset_ok = false;
let mut node = self.open_elements.current_node();
while let Some(current) = node {
if self.node_has_html_name(current, "li") {
self.open_elements
.generate_implied_end_tags(&self.document, Some("li"));
self.pop_until_one_of_popped(&["li"]);
break;
}
if is_special(&self.document, current)
&& !["address", "div", "p"]
.iter()
.any(|&name| self.node_has_html_name(current, name))
{
break;
}
node = self.open_elements.element_immediately_above(current);
}
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "dd" | "dt") => {
self.frameset_ok = false;
let mut node = self.open_elements.current_node();
while let Some(current) = node {
if self.node_has_html_name(current, "dd") {
self.open_elements
.generate_implied_end_tags(&self.document, Some("dd"));
self.pop_until_one_of_popped(&["dd"]);
break;
}
if self.node_has_html_name(current, "dt") {
self.open_elements
.generate_implied_end_tags(&self.document, Some("dt"));
self.pop_until_one_of_popped(&["dt"]);
break;
}
if is_special(&self.document, current)
&& !["address", "div", "p"]
.iter()
.any(|&name| self.node_has_html_name(current, name))
{
break;
}
node = self.open_elements.element_immediately_above(current);
}
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "plaintext" => {
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(Some(ExternalState::PlainText))
}
TokenKind::StartTag(tag) if tag.name == "button" => {
if self
.open_elements
.has_element_in_scope(&self.document, "button")
{
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&["button"]);
}
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
self.frameset_ok = false;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"address"
| "article"
| "aside"
| "blockquote"
| "button"
| "center"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "header"
| "hgroup"
| "listing"
| "main"
| "menu"
| "nav"
| "ol"
| "pre"
| "search"
| "section"
| "select"
| "summary"
| "ul"
) =>
{
if !self
.open_elements
.has_element_in_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&[tag.name.as_str()]);
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "form" => {
if self.has_template_on_stack() {
if !self
.open_elements
.has_element_in_scope(&self.document, "form")
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&["form"]);
} else {
let Some(node) = self.form_element_pointer.take() else {
return TokenOutcome::Consumed(None);
};
if !self.has_node_in_scope(node) {
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.remove_node_from_open_elements(node);
}
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "p" => {
if !self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
let p_tag = TagToken {
name: "p".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_html_element(&p_tag, None);
}
self.close_a_p_element();
let _ = tag;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "li" => {
if !self
.open_elements
.has_element_in_list_item_scope(&self.document, "li")
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, Some("li"));
self.pop_until_one_of_popped(&["li"]);
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "dd" | "dt") => {
if !self
.open_elements
.has_element_in_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, Some(tag.name.as_str()));
self.pop_until_one_of_popped(&[tag.name.as_str()]);
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(tag.name.as_str(), "h1" | "h2" | "h3" | "h4" | "h5" | "h6") =>
{
let headings = ["h1", "h2", "h3", "h4", "h5", "h6"];
if !headings.iter().any(|&heading| {
self.open_elements
.has_element_in_scope(&self.document, heading)
}) {
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&headings);
let _ = tag;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "a" => {
let marker_boundary = self
.active_formatting_elements
.entries
.iter()
.rposition(|entry| matches!(entry, FormattingEntry::Marker))
.map_or(0, |marker_index| marker_index + 1);
let existing_a = self.active_formatting_elements.entries[marker_boundary..]
.iter()
.find_map(|entry| match entry {
FormattingEntry::Element(node) if self.node_has_html_name(*node, "a") => {
Some(*node)
}
_ => None,
});
if let Some(existing_a) = existing_a {
self.adoption_agency_algorithm(tag);
self.remove_node_from_active_formatting_elements(existing_a);
self.remove_node_from_open_elements(existing_a);
}
self.reconstruct_the_active_formatting_elements();
let element = self.insert_html_element(tag, Some(position));
self.active_formatting_elements
.push(&self.document, element);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"b" | "big"
| "code"
| "em"
| "font"
| "i"
| "s"
| "small"
| "strike"
| "strong"
| "tt"
| "u"
) =>
{
self.reconstruct_the_active_formatting_elements();
let element = self.insert_html_element(tag, Some(position));
self.active_formatting_elements
.push(&self.document, element);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "nobr" => {
self.reconstruct_the_active_formatting_elements();
if self
.open_elements
.has_element_in_scope(&self.document, "nobr")
{
self.adoption_agency_algorithm(tag);
self.reconstruct_the_active_formatting_elements();
}
let element = self.insert_html_element(tag, Some(position));
self.active_formatting_elements
.push(&self.document, element);
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"a" | "b"
| "big"
| "code"
| "em"
| "font"
| "i"
| "nobr"
| "s"
| "small"
| "strike"
| "strong"
| "tt"
| "u"
) =>
{
self.adoption_agency_algorithm(tag);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(tag.name.as_str(), "applet" | "marquee" | "object") =>
{
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
self.active_formatting_elements.push_marker();
self.frameset_ok = false;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(tag.name.as_str(), "applet" | "marquee" | "object") =>
{
if !self
.open_elements
.has_element_in_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&[tag.name.as_str()]);
self.active_formatting_elements.clear_up_to_last_marker();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "table" => {
if self.quirks_mode != QuirksMode::Quirks
&& self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.insert_html_element(tag, Some(position));
self.frameset_ok = false;
self.insertion_mode = InsertionMode::InTable;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "br" => {
let br_tag = TagToken {
name: "br".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_void_element(&br_tag, Some(position));
let _ = tag;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"area" | "br" | "embed" | "img" | "keygen" | "wbr"
) =>
{
self.insert_void_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "input" => {
if self
.open_elements
.has_element_in_scope(&self.document, "select")
{
self.pop_until_one_of_popped(&["select"]);
}
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
let is_hidden_type = tag
.attributes
.iter()
.find(|attribute| attribute.name == "type")
.is_some_and(|attribute| attribute.value.eq_ignore_ascii_case("hidden"));
if !is_hidden_type {
self.frameset_ok = false;
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(tag.name.as_str(), "param" | "source" | "track") =>
{
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "hr" => {
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
if self
.open_elements
.has_element_in_scope(&self.document, "select")
{
self.open_elements
.generate_implied_end_tags(&self.document, None);
}
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
self.frameset_ok = false;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "image" => {
let img_tag = TagToken {
name: "img".to_owned(),
..tag.clone()
};
self.process_token_in_body(&TokenKind::StartTag(img_tag), position)
}
TokenKind::StartTag(tag) if tag.name == "textarea" => {
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::Rcdata,
Some(position),
);
self.skip_next_line_feed = true;
self.frameset_ok = false;
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "xmp" => {
if self
.open_elements
.has_element_in_button_scope(&self.document, "p")
{
self.close_a_p_element();
}
self.reconstruct_the_active_formatting_elements();
self.frameset_ok = false;
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::RawText,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "iframe" => {
self.frameset_ok = false;
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::RawText,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "noembed" => {
let (_, state) = self.generic_text_element_parsing_algorithm(
tag,
GenericTextElementKind::RawText,
Some(position),
);
TokenOutcome::Consumed(Some(state))
}
TokenKind::StartTag(tag) if tag.name == "select" => {
if self
.open_elements
.has_element_in_scope(&self.document, "select")
{
self.pop_until_one_of_popped(&["select"]);
} else {
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
self.frameset_ok = false;
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "option" => {
self.with_option_pop_hook(|this| {
if this
.open_elements
.has_element_in_scope(&this.document, "select")
{
this.open_elements
.generate_implied_end_tags(&this.document, Some("optgroup"));
} else if this
.open_elements
.current_node()
.is_some_and(|node| this.node_has_html_name(node, "option"))
{
this.open_elements.pop();
}
});
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "optgroup" => {
self.with_option_pop_hook(|this| {
if this
.open_elements
.has_element_in_scope(&this.document, "select")
{
this.open_elements
.generate_implied_end_tags(&this.document, None);
} else if this
.open_elements
.current_node()
.is_some_and(|node| this.node_has_html_name(node, "option"))
{
this.open_elements.pop();
}
});
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "rb" | "rtc") => {
if self
.open_elements
.has_element_in_scope(&self.document, "ruby")
{
self.open_elements
.generate_implied_end_tags(&self.document, None);
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "rp" | "rt") => {
if self
.open_elements
.has_element_in_scope(&self.document, "ruby")
{
self.open_elements
.generate_implied_end_tags(&self.document, Some("rtc"));
}
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "math" => {
self.reconstruct_the_active_formatting_elements();
self.insert_foreign_element(tag, MATHML_NAMESPACE, false, Some(position));
if tag.self_closing {
self.open_elements.pop();
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "svg" => {
self.reconstruct_the_active_formatting_elements();
self.insert_foreign_element(tag, SVG_NAMESPACE, false, Some(position));
if tag.self_closing {
self.open_elements.pop();
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption"
| "col"
| "colgroup"
| "frame"
| "head"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
let _ = tag;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) => {
self.reconstruct_the_active_formatting_elements();
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) => {
self.any_other_end_tag_in_body(tag);
TokenOutcome::Consumed(None)
}
}
}
fn process_token_text(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) => {
if self.skip_next_line_feed {
self.skip_next_line_feed = false;
if *c == '\n' {
return TokenOutcome::Consumed(None);
}
}
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Eof => {
self.open_elements.pop();
self.insertion_mode = self.original_insertion_mode.take().expect(
"\"text\" mode is only entered via \
generic_text_element_parsing_algorithm, which always records \
original_insertion_mode first",
);
TokenOutcome::Reprocess
}
TokenKind::EndTag(_) => {
self.open_elements.pop();
self.insertion_mode = self.original_insertion_mode.take().expect(
"\"text\" mode is only entered via \
generic_text_element_parsing_algorithm, which always records \
original_insertion_mode first",
);
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(_)
| TokenKind::Comment(_)
| TokenKind::ProcessingInstruction(_)
| TokenKind::Doctype(_) => unreachable!(
"the tokenizer's RCDATA/RAWTEXT/script-data states only ever produce \
character and end-tag tokens (plus EOF) — never start tags, comments, \
processing instructions, or DOCTYPEs"
),
}
}
fn in_table_anything_else(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
self.foster_parenting = true;
let outcome = self.process_token_in_body(kind, position);
self.foster_parenting = false;
outcome
}
fn process_token_in_table(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(_)
if self.open_elements.current_node().is_some_and(|node| {
["table", "tbody", "template", "tfoot", "thead", "tr"]
.iter()
.any(|&name| self.node_has_html_name(node, name))
}) =>
{
self.pending_table_character_tokens.clear();
self.original_insertion_mode = Some(self.insertion_mode);
self.insertion_mode = InsertionMode::InTableText;
TokenOutcome::Reprocess
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "caption" => {
self.clear_stack_back_to_a_table_context();
self.active_formatting_elements.push_marker();
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InCaption;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "colgroup" => {
self.clear_stack_back_to_a_table_context();
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InColumnGroup;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "col" => {
self.clear_stack_back_to_a_table_context();
let colgroup_tag = TagToken {
name: "colgroup".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_html_element(&colgroup_tag, None);
self.insertion_mode = InsertionMode::InColumnGroup;
TokenOutcome::Reprocess
}
TokenKind::StartTag(tag)
if matches!(tag.name.as_str(), "tbody" | "tfoot" | "thead") =>
{
self.clear_stack_back_to_a_table_context();
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InTableBody;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "td" | "th" | "tr") => {
self.clear_stack_back_to_a_table_context();
let tbody_tag = TagToken {
name: "tbody".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_html_element(&tbody_tag, None);
self.insertion_mode = InsertionMode::InTableBody;
TokenOutcome::Reprocess
}
TokenKind::StartTag(tag) if tag.name == "table" => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, "table")
{
return TokenOutcome::Consumed(None);
}
self.pop_until_one_of_popped(&["table"]);
self.reset_the_insertion_mode_appropriately();
TokenOutcome::Reprocess
}
TokenKind::EndTag(tag) if tag.name == "table" => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, "table")
{
return TokenOutcome::Consumed(None);
}
self.pop_until_one_of_popped(&["table"]);
self.reset_the_insertion_mode_appropriately();
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"body"
| "caption"
| "col"
| "colgroup"
| "html"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(tag.name.as_str(), "style" | "script" | "template") =>
{
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "input" => {
let is_hidden_type = tag
.attributes
.iter()
.find(|attribute| attribute.name == "type")
.is_some_and(|attribute| attribute.value.eq_ignore_ascii_case("hidden"));
if !is_hidden_type {
return self.in_table_anything_else(kind, position);
}
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "form" => {
if self.has_template_on_stack() || self.form_element_pointer.is_some() {
return TokenOutcome::Consumed(None);
}
let form = self.insert_html_element(tag, Some(position));
self.form_element_pointer = Some(form);
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::Eof => self.process_token_in_body(kind, position),
_ => self.in_table_anything_else(kind, position),
}
}
fn process_token_in_table_text(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Character(c) if *c == '\0' => TokenOutcome::Consumed(None),
TokenKind::Character(c) => {
self.pending_table_character_tokens.push((*c, position));
TokenOutcome::Consumed(None)
}
_ => {
let tokens = std::mem::take(&mut self.pending_table_character_tokens);
let has_non_whitespace = tokens.iter().any(|&(c, _)| !is_whitespace(c));
if has_non_whitespace {
for &(c, char_position) in &tokens {
self.in_table_anything_else(&TokenKind::Character(c), char_position);
}
} else {
for &(c, char_position) in &tokens {
self.insert_character(c, Some(char_position));
}
}
self.insertion_mode = self.original_insertion_mode.take().expect(
"\"in table text\" is only entered from \"in table\", which always \
records original_insertion_mode first",
);
TokenOutcome::Reprocess
}
}
}
fn close_caption(&mut self) -> bool {
if !self
.open_elements
.has_element_in_table_scope(&self.document, "caption")
{
return false;
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&["caption"]);
self.active_formatting_elements.clear_up_to_last_marker();
self.insertion_mode = InsertionMode::InTable;
true
}
fn process_token_in_caption(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::EndTag(tag) if tag.name == "caption" => {
self.close_caption();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption"
| "col"
| "colgroup"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
if self.close_caption() {
TokenOutcome::Reprocess
} else {
TokenOutcome::Consumed(None)
}
}
TokenKind::EndTag(tag) if tag.name == "table" => {
if self.close_caption() {
TokenOutcome::Reprocess
} else {
TokenOutcome::Consumed(None)
}
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"body"
| "col"
| "colgroup"
| "html"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
TokenOutcome::Consumed(None)
}
_ => self.process_token_in_body(kind, position),
}
}
fn process_token_in_column_group(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "col" => {
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "colgroup" => {
if !self
.open_elements
.current_node()
.is_some_and(|node| self.node_has_html_name(node, "colgroup"))
{
return TokenOutcome::Consumed(None);
}
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTable;
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "col" => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::Eof => self.process_token_in_body(kind, position),
_ => {
if !self
.open_elements
.current_node()
.is_some_and(|node| self.node_has_html_name(node, "colgroup"))
{
return TokenOutcome::Consumed(None);
}
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTable;
TokenOutcome::Reprocess
}
}
}
fn close_table_body_and_reprocess(&mut self) -> TokenOutcome {
let in_scope = ["tbody", "thead", "tfoot"].iter().any(|&name| {
self.open_elements
.has_element_in_table_scope(&self.document, name)
});
if !in_scope {
return TokenOutcome::Consumed(None);
}
self.clear_stack_back_to_a_table_body_context();
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTable;
TokenOutcome::Reprocess
}
fn process_token_in_table_body(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::StartTag(tag) if tag.name == "tr" => {
self.clear_stack_back_to_a_table_body_context();
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InRow;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "th" | "td") => {
self.clear_stack_back_to_a_table_body_context();
let tr_tag = TagToken {
name: "tr".to_owned(),
self_closing: false,
attributes: vec![],
};
self.insert_html_element(&tr_tag, None);
self.insertion_mode = InsertionMode::InRow;
TokenOutcome::Reprocess
}
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "tbody" | "tfoot" | "thead") => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.clear_stack_back_to_a_table_body_context();
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTable;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead"
) =>
{
self.close_table_body_and_reprocess()
}
TokenKind::EndTag(tag) if tag.name == "table" => self.close_table_body_and_reprocess(),
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"body" | "caption" | "col" | "colgroup" | "html" | "td" | "th" | "tr"
) =>
{
TokenOutcome::Consumed(None)
}
_ => self.process_token_in_table(kind, position),
}
}
fn close_row_and_reprocess(&mut self) -> TokenOutcome {
if !self
.open_elements
.has_element_in_table_scope(&self.document, "tr")
{
return TokenOutcome::Consumed(None);
}
self.clear_stack_back_to_a_table_row_context();
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTableBody;
TokenOutcome::Reprocess
}
fn process_token_in_row(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "th" | "td") => {
self.clear_stack_back_to_a_table_row_context();
self.insert_html_element(tag, Some(position));
self.insertion_mode = InsertionMode::InCell;
self.active_formatting_elements.push_marker();
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "tr" => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, "tr")
{
return TokenOutcome::Consumed(None);
}
self.clear_stack_back_to_a_table_row_context();
self.open_elements.pop();
self.insertion_mode = InsertionMode::InTableBody;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead" | "tr"
) =>
{
self.close_row_and_reprocess()
}
TokenKind::EndTag(tag) if tag.name == "table" => self.close_row_and_reprocess(),
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "tbody" | "tfoot" | "thead") => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.close_row_and_reprocess()
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"body" | "caption" | "col" | "colgroup" | "html" | "td" | "th"
) =>
{
TokenOutcome::Consumed(None)
}
_ => self.process_token_in_table(kind, position),
}
}
fn close_the_cell(&mut self) {
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&["td", "th"]);
self.active_formatting_elements.clear_up_to_last_marker();
self.insertion_mode = InsertionMode::InRow;
}
fn process_token_in_cell(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::EndTag(tag) if matches!(tag.name.as_str(), "td" | "th") => {
if !self
.open_elements
.has_element_in_table_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.open_elements
.generate_implied_end_tags(&self.document, None);
self.pop_until_one_of_popped(&[tag.name.as_str()]);
self.active_formatting_elements.clear_up_to_last_marker();
self.insertion_mode = InsertionMode::InRow;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption"
| "col"
| "colgroup"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
self.close_the_cell();
TokenOutcome::Reprocess
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"body" | "caption" | "col" | "colgroup" | "html"
) =>
{
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag)
if matches!(
tag.name.as_str(),
"table" | "tbody" | "tfoot" | "thead" | "tr"
) =>
{
if !self
.open_elements
.has_element_in_table_scope(&self.document, &tag.name)
{
return TokenOutcome::Consumed(None);
}
self.close_the_cell();
TokenOutcome::Reprocess
}
_ => self.process_token_in_body(kind, position),
}
}
fn switch_from_in_template_to(&mut self, mode: InsertionMode) -> TokenOutcome {
self.stack_of_template_insertion_modes.pop();
self.stack_of_template_insertion_modes.push(mode);
self.insertion_mode = mode;
TokenOutcome::Reprocess
}
fn process_token_in_template(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(_)
| TokenKind::Comment(_)
| TokenKind::ProcessingInstruction(_)
| TokenKind::Doctype(_) => self.process_token_in_body(kind, position),
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"base"
| "basefont"
| "bgsound"
| "link"
| "meta"
| "noframes"
| "script"
| "style"
| "template"
| "title"
) =>
{
self.process_token_in_head(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "template" => {
self.process_token_in_head(kind, position)
}
TokenKind::StartTag(tag)
if matches!(
tag.name.as_str(),
"caption" | "colgroup" | "tbody" | "tfoot" | "thead"
) =>
{
self.switch_from_in_template_to(InsertionMode::InTable)
}
TokenKind::StartTag(tag) if tag.name == "col" => {
self.switch_from_in_template_to(InsertionMode::InColumnGroup)
}
TokenKind::StartTag(tag) if tag.name == "tr" => {
self.switch_from_in_template_to(InsertionMode::InTableBody)
}
TokenKind::StartTag(tag) if matches!(tag.name.as_str(), "td" | "th") => {
self.switch_from_in_template_to(InsertionMode::InRow)
}
TokenKind::StartTag(_) => self.switch_from_in_template_to(InsertionMode::InBody),
TokenKind::EndTag(_) => TokenOutcome::Consumed(None),
TokenKind::Eof => {
self.pop_until_one_of_popped(&["template"]);
self.active_formatting_elements.clear_up_to_last_marker();
self.stack_of_template_insertion_modes.pop();
self.reset_the_insertion_mode_appropriately();
TokenOutcome::Reprocess
}
}
}
fn process_token_after_body(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.process_token_in_body(kind, position)
}
TokenKind::Comment(data) => {
self.append_comment_to_html_element(data.clone(), position);
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.append_processing_instruction_to_html_element(
pi.target.clone(),
pi.data.clone(),
position,
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "html" => {
let _ = tag;
self.insertion_mode = InsertionMode::AfterAfterBody;
TokenOutcome::Consumed(None)
}
TokenKind::Eof => {
TokenOutcome::Consumed(None)
}
_ => {
self.insertion_mode = InsertionMode::InBody;
TokenOutcome::Reprocess
}
}
}
fn process_token_in_frameset(&mut self, kind: &TokenKind, position: Position) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "frameset" => {
self.insert_html_element(tag, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::EndTag(tag) if tag.name == "frameset" => {
let _ = tag;
self.open_elements.pop();
let current_is_frameset = self
.open_elements
.current_node()
.is_some_and(|node| self.node_has_html_name(node, "frameset"));
if !current_is_frameset {
self.insertion_mode = InsertionMode::AfterFrameset;
}
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "frame" => {
self.insert_html_element(tag, Some(position));
self.open_elements.pop();
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "noframes" => {
self.process_token_in_head(kind, position)
}
TokenKind::Eof => {
TokenOutcome::Consumed(None)
}
_ => TokenOutcome::Consumed(None),
}
}
fn process_token_after_frameset(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Character(c) if is_whitespace(*c) => {
self.insert_character(*c, Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::Comment(data) => {
self.insert_comment(data.clone(), Some(position));
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.insert_processing_instruction(
pi.target.clone(),
pi.data.clone(),
Some(position),
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::EndTag(tag) if tag.name == "html" => {
let _ = tag;
self.insertion_mode = InsertionMode::AfterAfterFrameset;
TokenOutcome::Consumed(None)
}
TokenKind::StartTag(tag) if tag.name == "noframes" => {
self.process_token_in_head(kind, position)
}
TokenKind::Eof => TokenOutcome::Consumed(None),
_ => TokenOutcome::Consumed(None),
}
}
fn process_token_after_after_body(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Comment(data) => {
self.append_comment_to_document(data.clone(), position);
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.append_processing_instruction_to_document(
pi.target.clone(),
pi.data.clone(),
position,
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => self.process_token_in_body(kind, position),
TokenKind::Character(c) if is_whitespace(*c) => {
self.process_token_in_body(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::Eof => TokenOutcome::Consumed(None),
_ => {
self.insertion_mode = InsertionMode::InBody;
TokenOutcome::Reprocess
}
}
}
fn process_token_after_after_frameset(
&mut self,
kind: &TokenKind,
position: Position,
) -> TokenOutcome {
match kind {
TokenKind::Comment(data) => {
self.append_comment_to_document(data.clone(), position);
TokenOutcome::Consumed(None)
}
TokenKind::ProcessingInstruction(pi) => {
self.append_processing_instruction_to_document(
pi.target.clone(),
pi.data.clone(),
position,
);
TokenOutcome::Consumed(None)
}
TokenKind::Doctype(_) => self.process_token_in_body(kind, position),
TokenKind::Character(c) if is_whitespace(*c) => {
self.process_token_in_body(kind, position)
}
TokenKind::StartTag(tag) if tag.name == "html" => {
self.process_token_in_body(kind, position)
}
TokenKind::Eof => TokenOutcome::Consumed(None),
TokenKind::StartTag(tag) if tag.name == "noframes" => {
self.process_token_in_head(kind, position)
}
_ => TokenOutcome::Consumed(None),
}
}
}
#[cfg(test)]
mod tests {
use super::{HTML_NAMESPACE, OpenElementsStack};
use crate::document::{Document, NodeKind};
fn stack_of(document: &mut Document, names: &[&str]) -> OpenElementsStack {
let mut stack = OpenElementsStack::new();
for &name in names {
let node = document.new_node(
NodeKind::Element {
name: name.to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
stack.push(node);
}
stack
}
#[test]
fn empty_stack_has_no_current_node() {
let stack = OpenElementsStack::new();
assert_eq!(stack.current_node(), None);
}
#[test]
fn push_and_pop_and_current_node() {
let mut document = Document::new();
let mut stack = stack_of(&mut document, &["html", "body"]);
let body = stack.current_node().unwrap();
assert_eq!(
document.node(body).kind,
NodeKind::Element {
name: "body".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
stack.pop();
let html = stack.current_node().unwrap();
assert_eq!(
document.node(html).kind,
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn has_element_in_scope_finds_a_matching_ancestor() {
let mut document = Document::new();
let stack = stack_of(&mut document, &["html", "body", "div", "p"]);
assert!(stack.has_element_in_scope(&document, "div"));
assert!(stack.has_element_in_scope(&document, "p"));
assert!(!stack.has_element_in_scope(&document, "span"));
}
#[test]
fn has_element_in_scope_stops_at_a_boundary_element() {
let mut document = Document::new();
let stack = stack_of(&mut document, &["html", "body", "p", "table", "tr", "td"]);
assert!(!stack.has_element_in_scope(&document, "p"));
assert!(!stack.has_element_in_scope(&document, "table"));
assert!(stack.has_element_in_scope(&document, "td"));
}
#[test]
fn has_element_in_list_item_scope_stops_at_ul_but_default_scope_does_not() {
let mut document = Document::new();
let stack = stack_of(&mut document, &["html", "body", "ul", "li", "span"]);
assert!(!stack.has_element_in_list_item_scope(&document, "body"));
assert!(stack.has_element_in_scope(&document, "body"));
}
#[test]
fn has_element_in_button_scope_stops_at_button() {
let mut document = Document::new();
let stack = stack_of(&mut document, &["html", "body", "p", "button"]);
assert!(!stack.has_element_in_button_scope(&document, "p"));
assert!(stack.has_element_in_scope(&document, "p"));
}
#[test]
fn has_element_in_table_scope_uses_its_own_short_boundary_list() {
let mut document = Document::new();
let stack = stack_of(&mut document, &["html", "table", "td", "div"]);
assert!(stack.has_element_in_table_scope(&document, "table"));
assert!(!stack.has_element_in_scope(&document, "table"));
}
#[test]
fn generate_implied_end_tags_pops_while_current_node_is_in_the_list() {
let mut document = Document::new();
let mut stack = stack_of(&mut document, &["html", "body", "ul", "li", "p"]);
stack.generate_implied_end_tags(&document, None);
let current = stack.current_node().unwrap();
assert_eq!(
document.node(current).kind,
NodeKind::Element {
name: "ul".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn generate_implied_end_tags_excludes_the_given_element() {
let mut document = Document::new();
let mut stack = stack_of(&mut document, &["html", "body", "dl", "dt", "li"]);
stack.generate_implied_end_tags(&document, Some("li"));
let current = stack.current_node().unwrap();
assert_eq!(
document.node(current).kind,
NodeKind::Element {
name: "li".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn generate_implied_end_tags_thoroughly_pops_table_elements_too() {
let mut document = Document::new();
let mut stack = stack_of(&mut document, &["html", "table", "tbody", "tr", "td"]);
stack.generate_implied_end_tags(&document, None);
assert_eq!(stack.entries.len(), 5);
stack.generate_implied_end_tags_thoroughly(&document);
let current = stack.current_node().unwrap();
assert_eq!(
document.node(current).kind,
NodeKind::Element {
name: "table".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
}
#[cfg(test)]
mod quirks_mode_tests {
use super::{QuirksMode, determine_quirks_mode};
use crate::tokenizer::DoctypeToken;
fn doctype(
name: Option<&str>,
public_identifier: Option<&str>,
system_identifier: Option<&str>,
force_quirks: bool,
) -> DoctypeToken {
DoctypeToken {
name: name.map(str::to_owned),
public_identifier: public_identifier.map(str::to_owned),
system_identifier: system_identifier.map(str::to_owned),
force_quirks,
}
}
#[test]
fn html5_doctype_is_no_quirks() {
let token = doctype(Some("html"), None, None, false);
assert_eq!(determine_quirks_mode(&token), QuirksMode::NoQuirks);
}
#[test]
fn force_quirks_flag_wins_regardless_of_everything_else() {
let token = doctype(Some("html"), None, None, true);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
#[test]
fn missing_doctype_name_is_quirks() {
let token = doctype(None, None, None, false);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
#[test]
fn exact_public_identifier_match_is_quirks_case_insensitively() {
let token = doctype(Some("html"), Some("html"), None, false);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
#[test]
fn html4_transitional_without_system_identifier_is_quirks() {
let token = doctype(
Some("html"),
Some("-//W3C//DTD HTML 4.01 Transitional//EN"),
None,
false,
);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
#[test]
fn html4_transitional_with_system_identifier_is_limited_quirks() {
let token = doctype(
Some("html"),
Some("-//W3C//DTD HTML 4.01 Transitional//EN"),
Some("http://www.w3.org/TR/html4/loose.dtd"),
false,
);
assert_eq!(determine_quirks_mode(&token), QuirksMode::LimitedQuirks);
}
#[test]
fn xhtml_transitional_is_always_limited_quirks() {
let token = doctype(
Some("html"),
Some("-//W3C//DTD XHTML 1.0 Transitional//EN"),
Some("http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"),
false,
);
assert_eq!(determine_quirks_mode(&token), QuirksMode::LimitedQuirks);
}
#[test]
fn empty_system_identifier_counts_as_missing_for_the_html4_split() {
let token = doctype(
Some("html"),
Some("-//W3C//DTD HTML 4.01 Frameset//EN"),
Some(""),
false,
);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
#[test]
fn ibm_system_identifier_is_quirks() {
let token = doctype(
Some("html"),
None,
Some("http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"),
false,
);
assert_eq!(determine_quirks_mode(&token), QuirksMode::Quirks);
}
}
#[cfg(test)]
mod insertion_algorithm_tests {
use super::{HTML_NAMESPACE, InsertionLocation, TreeBuilder};
use crate::document::{NodeId, NodeKind};
use crate::tokenizer::TagToken;
fn tag(name: &str) -> TagToken {
TagToken {
name: name.to_owned(),
self_closing: false,
attributes: vec![],
}
}
fn bootstrap_html(builder: &mut TreeBuilder) -> NodeId {
let root = builder.document.root();
let html = builder.document.new_node(
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
builder.document.append_child(root, html);
builder.open_elements.push(html);
html
}
fn only_child_kind(builder: &TreeBuilder, parent: NodeId) -> NodeKind {
let children: Vec<_> = builder.document.children(parent).collect();
assert_eq!(children.len(), 1, "expected exactly one child");
builder.document.node(children[0]).kind.clone()
}
#[test]
fn insert_html_element_nests_under_the_current_node() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
assert_eq!(
builder.document.children(html).collect::<Vec<_>>(),
vec![body]
);
assert_eq!(builder.open_elements.current_node(), Some(body));
assert_eq!(
builder.document.node(body).kind,
NodeKind::Element {
name: "body".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn generic_rcdata_element_parsing_algorithm() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
builder.insertion_mode = super::InsertionMode::InBody;
let (title, external_state) = builder.generic_text_element_parsing_algorithm(
&tag("title"),
super::GenericTextElementKind::Rcdata,
None,
);
assert_eq!(external_state, crate::tokenizer::ExternalState::RcData);
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![title]
);
assert_eq!(builder.open_elements.current_node(), Some(title));
assert_eq!(
builder.original_insertion_mode,
Some(super::InsertionMode::InBody)
);
assert_eq!(builder.insertion_mode, super::InsertionMode::Text);
}
#[test]
fn generic_rawtext_element_parsing_algorithm() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
builder.insert_html_element(&tag("head"), None);
builder.insertion_mode = super::InsertionMode::InHead;
let (style, external_state) = builder.generic_text_element_parsing_algorithm(
&tag("style"),
super::GenericTextElementKind::RawText,
None,
);
assert_eq!(external_state, crate::tokenizer::ExternalState::RawText);
assert_eq!(builder.open_elements.current_node(), Some(style));
assert_eq!(
builder.original_insertion_mode,
Some(super::InsertionMode::InHead)
);
assert_eq!(builder.insertion_mode, super::InsertionMode::Text);
}
#[test]
fn a_second_element_is_never_appended_directly_to_the_document_root() {
let mut builder = TreeBuilder::new();
let root = builder.document.root();
bootstrap_html(&mut builder);
let second_html = builder.document.new_node(
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
builder.insert_element_at_adjusted_insertion_location(
second_html,
InsertionLocation {
parent: root,
before: None,
},
);
assert_eq!(builder.document.children(root).count(), 1);
}
#[test]
fn insert_character_merges_into_an_adjacent_text_node() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
builder.insert_character('a', None);
builder.insert_character('b', None);
assert_eq!(
only_child_kind(&builder, html),
NodeKind::Text {
content: "ab".to_owned()
}
);
}
#[test]
fn insert_character_after_an_element_creates_a_new_text_node() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
builder.insert_html_element(&tag("body"), None);
builder.open_elements.pop();
builder.insert_character('x', None);
let children: Vec<_> = builder.document.children(html).collect();
assert_eq!(children.len(), 2);
assert_eq!(
builder.document.node(children[1]).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn insert_comment_and_processing_instruction() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
builder.insert_comment("hi".to_owned(), None);
builder.insert_processing_instruction("target".to_owned(), "data".to_owned(), None);
let children: Vec<_> = builder.document.children(html).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Comment {
content: "hi".to_owned()
}
);
assert_eq!(
builder.document.node(children[1]).kind,
NodeKind::ProcessingInstruction {
target: "target".to_owned(),
data: "data".to_owned(),
}
);
}
#[test]
fn foster_parenting_inserts_before_the_table_not_inside_it() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
let table = builder.insert_html_element(&tag("table"), None);
builder.foster_parenting = true;
builder.insert_character('x', None);
assert_eq!(builder.document.children(table).count(), 0);
let html_children: Vec<_> = builder.document.children(html).collect();
assert_eq!(html_children.len(), 2);
assert_eq!(html_children[1], table);
assert_eq!(
builder.document.node(html_children[0]).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn foster_parenting_with_no_table_falls_back_to_the_html_element() {
let mut builder = TreeBuilder::new();
let html = bootstrap_html(&mut builder);
builder.insert_html_element(&tag("tbody"), None);
builder.foster_parenting = true;
builder.insert_character('x', None);
let html_children: Vec<_> = builder.document.children(html).collect();
assert_eq!(
builder.document.node(html_children[1]).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
}
#[cfg(test)]
mod active_formatting_elements_tests {
use super::{ActiveFormattingElements, FormattingEntry, HTML_NAMESPACE, TreeBuilder};
use crate::document::{Attribute, Document, NodeId, NodeKind};
use crate::tokenizer::TagToken;
fn font_with_color(document: &mut Document, color: &str) -> NodeId {
document.new_node(
NodeKind::Element {
name: "font".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![Attribute {
name: "color".to_owned(),
value: color.to_owned(),
namespace: None,
}],
},
None,
)
}
fn elements(afe: &ActiveFormattingElements) -> Vec<NodeId> {
afe.entries
.iter()
.filter_map(|entry| match entry {
FormattingEntry::Element(node) => Some(*node),
FormattingEntry::Marker => None,
})
.collect()
}
#[test]
fn noahs_ark_clause_removes_the_earliest_of_four_identical_elements() {
let mut document = Document::new();
let mut afe = ActiveFormattingElements::new();
let nodes: Vec<_> = (0..4)
.map(|_| font_with_color(&mut document, "red"))
.collect();
for &node in &nodes {
afe.push(&document, node);
}
assert_eq!(elements(&afe), vec![nodes[1], nodes[2], nodes[3]]);
}
#[test]
fn noahs_ark_clause_ignores_elements_with_different_attributes() {
let mut document = Document::new();
let mut afe = ActiveFormattingElements::new();
let red: Vec<_> = (0..3)
.map(|_| font_with_color(&mut document, "red"))
.collect();
let blue = font_with_color(&mut document, "blue");
for &node in &red {
afe.push(&document, node);
}
afe.push(&document, blue);
assert_eq!(elements(&afe).len(), 4);
}
#[test]
fn noahs_ark_clause_only_counts_entries_after_the_last_marker() {
let mut document = Document::new();
let mut afe = ActiveFormattingElements::new();
for _ in 0..3 {
let node = font_with_color(&mut document, "red");
afe.push(&document, node);
}
afe.push_marker();
let node = font_with_color(&mut document, "red");
afe.push(&document, node);
assert_eq!(afe.entries.len(), 5);
}
#[test]
fn clear_up_to_last_marker_removes_the_marker_and_everything_after_it() {
let mut document = Document::new();
let mut afe = ActiveFormattingElements::new();
let before = font_with_color(&mut document, "red");
afe.push(&document, before);
afe.push_marker();
let after = font_with_color(&mut document, "blue");
afe.push(&document, after);
afe.clear_up_to_last_marker();
assert_eq!(elements(&afe), vec![before]);
}
fn tag(name: &str) -> TagToken {
TagToken {
name: name.to_owned(),
self_closing: false,
attributes: vec![],
}
}
fn bootstrap_html(builder: &mut TreeBuilder) -> NodeId {
let root = builder.document.root();
let html = builder.document.new_node(
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
builder.document.append_child(root, html);
builder.open_elements.push(html);
html
}
#[test]
fn reconstruct_does_nothing_when_the_list_is_empty() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
builder.insert_html_element(&tag("body"), None);
let stack_before = builder.open_elements.current_node();
builder.reconstruct_the_active_formatting_elements();
assert_eq!(builder.open_elements.current_node(), stack_before);
}
#[test]
fn reconstruct_does_nothing_when_the_last_entry_is_still_open() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
builder.reconstruct_the_active_formatting_elements();
assert_eq!(builder.open_elements.current_node(), Some(b));
}
#[test]
fn reconstruct_reopens_a_single_implicitly_closed_formatting_element() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
builder.open_elements.pop();
builder.reconstruct_the_active_formatting_elements();
let new_b = builder
.open_elements
.current_node()
.expect("reconstruct should have pushed a new element");
assert_ne!(new_b, b, "must be a freshly created node, not the old one");
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![b, new_b]
);
assert_eq!(
builder.document.node(new_b).kind,
NodeKind::Element {
name: "b".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
super::FormattingEntry::Element(new_b),
*builder.active_formatting_elements.entries.last().unwrap()
);
}
#[test]
fn reconstruct_reopens_nested_formatting_elements_in_the_original_order() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
let i = builder.insert_html_element(&tag("i"), None);
builder
.active_formatting_elements
.push(&builder.document, i);
builder.open_elements.pop();
builder.open_elements.pop();
builder.reconstruct_the_active_formatting_elements();
let body_children: Vec<_> = builder.document.children(body).collect();
assert_eq!(body_children.len(), 2);
assert_eq!(body_children[0], b);
assert_eq!(builder.document.children(b).collect::<Vec<_>>(), vec![i]);
let new_b = body_children[1];
assert_eq!(
builder.document.node(new_b).kind,
NodeKind::Element {
name: "b".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let new_b_children: Vec<_> = builder.document.children(new_b).collect();
assert_eq!(new_b_children.len(), 1);
assert_eq!(
builder.document.node(new_b_children[0]).kind,
NodeKind::Element {
name: "i".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.open_elements.current_node(),
Some(new_b_children[0])
);
}
}
#[cfg(test)]
mod adoption_agency_tests {
use super::TreeBuilder;
use crate::document::{NodeId, NodeKind};
use crate::tokenizer::TagToken;
fn tag(name: &str) -> TagToken {
TagToken {
name: name.to_owned(),
self_closing: false,
attributes: vec![],
}
}
fn bootstrap_html(builder: &mut TreeBuilder) -> NodeId {
let root = builder.document.root();
let html = builder.document.new_node(
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
},
None,
);
builder.document.append_child(root, html);
builder.open_elements.push(html);
html
}
#[test]
fn current_node_matching_and_not_active_is_just_popped() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder.adoption_agency_algorithm(&tag("b"));
assert_eq!(builder.open_elements.current_node(), Some(body));
assert_eq!(builder.document.children(body).collect::<Vec<_>>(), vec![b]);
}
#[test]
fn no_furthest_block_pops_up_to_and_including_the_formatting_element() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let p = builder.insert_html_element(&tag("p"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
builder.adoption_agency_algorithm(&tag("b"));
assert_eq!(builder.open_elements.current_node(), Some(p));
assert!(!builder.open_elements.contains(b));
assert!(builder.active_formatting_elements.entries.is_empty());
assert_eq!(builder.document.children(p).collect::<Vec<_>>(), vec![b]);
assert_eq!(builder.document.children(body).collect::<Vec<_>>(), vec![p]);
}
#[test]
fn not_in_scope_returns_without_changing_the_tree() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
let table = builder.insert_html_element(&tag("table"), None);
builder.adoption_agency_algorithm(&tag("b"));
assert_eq!(builder.open_elements.current_node(), Some(table));
assert!(builder.open_elements.contains(b));
assert_eq!(
builder.document.children(b).collect::<Vec<_>>(),
vec![table]
);
}
#[test]
fn relocates_special_content_out_of_the_misnested_formatting_element() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let b = builder.insert_html_element(&tag("b"), None);
builder
.active_formatting_elements
.push(&builder.document, b);
let div = builder.insert_html_element(&tag("div"), None);
let inner_text = builder.document.new_node(
NodeKind::Text {
content: "inner".to_owned(),
},
None,
);
builder.document.append_child(div, inner_text);
builder.adoption_agency_algorithm(&tag("b"));
assert_eq!(builder.open_elements.current_node(), Some(div));
assert!(!builder.open_elements.contains(b));
assert!(builder.active_formatting_elements.entries.is_empty());
let body_children: Vec<_> = builder.document.children(body).collect();
assert_eq!(body_children, vec![b, div]);
assert_eq!(builder.document.children(b).count(), 0);
let div_children: Vec<_> = builder.document.children(div).collect();
assert_eq!(div_children.len(), 1);
let new_b = div_children[0];
assert_ne!(new_b, b);
assert_eq!(
builder.document.node(new_b).kind,
NodeKind::Element {
name: "b".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.document.children(new_b).collect::<Vec<_>>(),
vec![inner_text]
);
}
#[test]
fn any_other_end_tag_pops_up_to_a_matching_ancestor() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
let body = builder.insert_html_element(&tag("body"), None);
let div = builder.insert_html_element(&tag("div"), None);
builder.insert_html_element(&tag("span"), None);
builder.any_other_end_tag_in_body(&tag("div"));
assert_eq!(builder.open_elements.current_node(), Some(body));
assert!(!builder.open_elements.contains(div));
}
#[test]
fn any_other_end_tag_gives_up_at_the_first_special_element() {
let mut builder = TreeBuilder::new();
bootstrap_html(&mut builder);
builder.insert_html_element(&tag("body"), None);
builder.insert_html_element(&tag("div"), None); let span = builder.insert_html_element(&tag("span"), None);
builder.any_other_end_tag_in_body(&tag("x"));
assert_eq!(builder.open_elements.current_node(), Some(span));
}
}
#[cfg(test)]
mod insertion_mode_tests {
use super::{FormattingEntry, InsertionMode, QuirksMode, TreeBuilder};
use crate::document::{NodeId, NodeKind};
use crate::tokenizer::{Attribute, DoctypeToken, ExternalState, Position, TagToken, TokenKind};
fn pos() -> Position {
Position {
line: 1,
column: 1,
byte_offset: 0,
}
}
fn doctype(
name: Option<&str>,
public_identifier: Option<&str>,
system_identifier: Option<&str>,
) -> TokenKind {
TokenKind::Doctype(DoctypeToken {
name: name.map(str::to_owned),
public_identifier: public_identifier.map(str::to_owned),
system_identifier: system_identifier.map(str::to_owned),
force_quirks: false,
})
}
fn start_tag(name: &str) -> TokenKind {
TokenKind::StartTag(TagToken {
name: name.to_owned(),
self_closing: false,
attributes: vec![],
})
}
fn start_tag_with_attrs(name: &str, attrs: &[(&str, &str)]) -> TokenKind {
TokenKind::StartTag(TagToken {
name: name.to_owned(),
self_closing: false,
attributes: attrs
.iter()
.map(|&(name, value)| Attribute {
name: name.to_owned(),
value: value.to_owned(),
})
.collect(),
})
}
fn end_tag(name: &str) -> TokenKind {
TokenKind::EndTag(TagToken {
name: name.to_owned(),
self_closing: false,
attributes: vec![],
})
}
#[test]
fn initial_mode_ignores_whitespace() {
let mut builder = TreeBuilder::new();
builder.process_token(&TokenKind::Character(' '), pos());
assert_eq!(builder.insertion_mode, InsertionMode::Initial);
assert_eq!(
builder.document.children(builder.document.root()).count(),
0
);
}
#[test]
fn initial_mode_inserts_comment_as_a_document_child() {
let mut builder = TreeBuilder::new();
builder.process_token(&TokenKind::Comment("hi".to_owned()), pos());
let root = builder.document.root();
let children: Vec<_> = builder.document.children(root).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Comment {
content: "hi".to_owned()
}
);
assert_eq!(builder.insertion_mode, InsertionMode::Initial);
}
#[test]
fn initial_mode_doctype_inserts_a_doctype_node_and_switches_mode() {
let mut builder = TreeBuilder::new();
builder.process_token(&doctype(Some("html"), None, None), pos());
let root = builder.document.root();
let children: Vec<_> = builder.document.children(root).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Doctype {
name: Some("html".to_owned()),
public_identifier: Some(String::new()),
system_identifier: Some(String::new()),
}
);
assert_eq!(builder.quirks_mode, QuirksMode::NoQuirks);
assert_eq!(builder.insertion_mode, InsertionMode::BeforeHtml);
}
#[test]
fn initial_mode_doctype_records_quirks_mode() {
let mut builder = TreeBuilder::new();
builder.process_token(&doctype(Some("html"), Some("HTML"), None), pos());
assert_eq!(builder.quirks_mode, QuirksMode::Quirks);
}
#[test]
fn end_to_end_doctype_then_html_then_head_reaches_in_head() {
let mut builder = TreeBuilder::new();
builder.process_token(&doctype(Some("html"), None, None), pos());
builder.process_token(&start_tag_with_attrs("html", &[("lang", "en")]), pos());
builder.process_token(&start_tag("head"), pos());
assert_eq!(builder.quirks_mode, QuirksMode::NoQuirks);
assert_eq!(builder.insertion_mode, InsertionMode::InHead);
let root = builder.document.root();
let root_children: Vec<_> = builder.document.children(root).collect();
assert_eq!(root_children.len(), 2);
let html = root_children[1];
assert_eq!(
builder.document.node(html).kind,
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![super::Attribute {
name: "lang".to_owned(),
value: "en".to_owned(),
namespace: None,
}],
}
);
let head = builder.document.children(html).next().unwrap();
assert_eq!(
builder.document.node(head).kind,
NodeKind::Element {
name: "head".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.head_element_pointer, Some(head));
assert_eq!(builder.open_elements.current_node(), Some(head));
}
#[test]
fn end_to_end_without_a_doctype_sets_quirks_mode_and_synthesizes_html_and_head() {
let mut builder = TreeBuilder::new();
builder.process_token(&start_tag("head"), pos());
assert_eq!(builder.quirks_mode, QuirksMode::Quirks);
assert_eq!(builder.insertion_mode, InsertionMode::InHead);
let root = builder.document.root();
let html = builder.document.children(root).next().unwrap();
assert_eq!(
builder.document.node(html).kind,
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let head = builder.document.children(html).next().unwrap();
assert_eq!(builder.head_element_pointer, Some(head));
assert!(builder.open_elements.contains(html));
}
#[test]
fn before_html_mode_anything_else_synthesizes_html_and_reprocesses() {
let mut builder = TreeBuilder::new();
builder.insertion_mode = InsertionMode::BeforeHtml;
let outcome = builder.process_token_before_html(&end_tag("br"), pos());
assert!(matches!(outcome, super::TokenOutcome::Reprocess));
assert_eq!(builder.insertion_mode, InsertionMode::BeforeHead);
let root = builder.document.root();
let html = builder.document.children(root).next().unwrap();
assert_eq!(
builder.document.node(html).kind,
NodeKind::Element {
name: "html".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.open_elements.current_node(), Some(html));
}
#[test]
fn before_head_mode_html_start_tag_merges_new_attributes_only() {
let mut builder = TreeBuilder::new();
builder.insertion_mode = InsertionMode::BeforeHtml;
builder.process_token(&start_tag_with_attrs("html", &[("lang", "en")]), pos());
assert_eq!(builder.insertion_mode, InsertionMode::BeforeHead);
builder.process_token_before_head(
&start_tag_with_attrs("html", &[("lang", "de"), ("dir", "ltr")]),
pos(),
);
let root = builder.document.root();
let html = builder.document.children(root).next().unwrap();
let NodeKind::Element { attributes, .. } = &builder.document.node(html).kind else {
unreachable!()
};
assert_eq!(attributes.len(), 2);
assert!(
attributes
.iter()
.any(|a| a.name == "lang" && a.value == "en")
);
assert!(
attributes
.iter()
.any(|a| a.name == "dir" && a.value == "ltr")
);
}
fn bootstrap_in_head(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
builder.process_token(&start_tag("head"), pos());
let root = builder.document.root();
let html = builder.document.children(root).next().unwrap();
let head = builder.open_elements.current_node().unwrap();
(html, head)
}
#[test]
fn in_head_inserts_whitespace_characters() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
builder.process_token(&TokenKind::Character(' '), pos());
let children: Vec<_> = builder.document.children(head).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: " ".to_owned()
}
);
}
#[test]
fn in_head_void_like_elements_are_inserted_then_immediately_popped() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("meta"), pos());
let children: Vec<_> = builder.document.children(head).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "meta".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.open_elements.current_node(), Some(head));
}
#[test]
fn in_head_title_switches_to_text_mode_with_rcdata() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
let state = builder.process_token(&start_tag("title"), pos());
assert_eq!(state, Some(ExternalState::RcData));
assert_eq!(builder.insertion_mode, InsertionMode::Text);
assert_eq!(builder.original_insertion_mode, Some(InsertionMode::InHead));
let title = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(head).collect::<Vec<_>>(),
vec![title]
);
}
#[test]
fn in_head_script_switches_to_text_mode_with_script_data() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
let state = builder.process_token(&start_tag("script"), pos());
assert_eq!(state, Some(ExternalState::ScriptData));
assert_eq!(builder.insertion_mode, InsertionMode::Text);
}
#[test]
fn in_head_noscript_start_tag_switches_mode() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("noscript"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InHeadNoscript);
}
#[test]
fn in_head_end_tag_head_pops_and_switches_to_after_head() {
let mut builder = TreeBuilder::new();
let (html, head) = bootstrap_in_head(&mut builder);
builder.process_token(&end_tag("head"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterHead);
assert_eq!(builder.open_elements.current_node(), Some(html));
assert!(!builder.open_elements.contains(head));
}
#[test]
fn in_head_template_round_trips_through_the_stack() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("template"), pos());
let template = builder.open_elements.current_node().unwrap();
assert_ne!(template, head);
builder.process_token(&end_tag("template"), pos());
assert_eq!(builder.open_elements.current_node(), Some(head));
assert!(!builder.open_elements.contains(template));
assert_eq!(
builder.document.children(head).collect::<Vec<_>>(),
vec![template]
);
}
#[test]
fn in_head_template_start_tag_pushes_marker_and_switches_to_in_template() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.frameset_ok = true;
builder.process_token(&start_tag("template"), pos());
let template = builder.open_elements.current_node().unwrap();
assert!(matches!(
builder.active_formatting_elements.entries.last(),
Some(FormattingEntry::Marker)
));
assert!(!builder.frameset_ok);
assert_eq!(builder.insertion_mode, InsertionMode::InTemplate);
assert_eq!(
builder.stack_of_template_insertion_modes,
vec![InsertionMode::InTemplate]
);
let content: Vec<_> = builder.document.children(template).collect();
assert_eq!(content.len(), 1);
assert_eq!(
builder.document.node(content[0]).kind,
NodeKind::DocumentFragment
);
}
#[test]
fn in_head_template_end_tag_resets_insertion_mode_via_the_stack() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("template"), pos());
builder.process_token(&end_tag("template"), pos());
assert!(builder.stack_of_template_insertion_modes.is_empty());
assert_eq!(builder.insertion_mode, InsertionMode::InHead);
}
#[test]
fn content_inserted_under_a_template_lands_in_its_content_fragment() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("template"), pos());
let template = builder.open_elements.current_node().unwrap();
let content = builder.document.children(template).next().unwrap();
builder.process_token(&start_tag("p"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
assert_eq!(
builder.stack_of_template_insertion_modes,
vec![InsertionMode::InBody]
);
let p = builder.open_elements.current_node().unwrap();
assert_eq!(builder.document.parent(p), Some(content));
assert_eq!(
builder.document.children(template).collect::<Vec<_>>(),
vec![content]
);
}
#[test]
fn in_template_tr_cascades_through_in_table_body_and_in_row() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("template"), pos());
let template = builder.open_elements.current_node().unwrap();
let content = builder.document.children(template).next().unwrap();
builder.process_token(&start_tag("tr"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InRow);
assert_eq!(
builder.stack_of_template_insertion_modes,
vec![InsertionMode::InTableBody]
);
let tr = builder.open_elements.current_node().unwrap();
assert_eq!(builder.document.parent(tr), Some(content));
builder.process_token(&start_tag("td"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InCell);
let td = builder.open_elements.current_node().unwrap();
assert_eq!(builder.document.parent(td), Some(tr));
}
#[test]
fn in_template_any_other_end_tag_is_ignored() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("template"), pos());
let current_before = builder.open_elements.current_node();
builder.process_token(&end_tag("p"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTemplate);
assert_eq!(builder.open_elements.current_node(), current_before);
}
#[test]
fn in_head_noscript_delegates_link_to_in_head() {
let mut builder = TreeBuilder::new();
bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("noscript"), pos());
let noscript = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("link"), pos());
assert_eq!(builder.open_elements.current_node(), Some(noscript));
let children: Vec<_> = builder.document.children(noscript).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "link".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn in_head_noscript_end_tag_noscript_returns_to_in_head() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
builder.process_token(&start_tag("noscript"), pos());
builder.process_token(&end_tag("noscript"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InHead);
assert_eq!(builder.open_elements.current_node(), Some(head));
}
#[test]
fn after_head_body_start_tag_switches_to_in_body() {
let mut builder = TreeBuilder::new();
let (html, head) = bootstrap_in_head(&mut builder);
builder.process_token(&end_tag("head"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterHead);
builder.process_token(&start_tag("body"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
let body = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(html).collect::<Vec<_>>(),
vec![head, body]
);
}
#[test]
fn after_head_delegates_link_by_temporarily_repushing_head() {
let mut builder = TreeBuilder::new();
let (_, head) = bootstrap_in_head(&mut builder);
builder.process_token(&end_tag("head"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterHead);
builder.process_token(&start_tag("link"), pos());
assert!(!builder.open_elements.contains(head));
let children: Vec<_> = builder.document.children(head).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "link".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.insertion_mode, InsertionMode::AfterHead);
}
#[test]
fn after_head_anything_else_synthesizes_body_and_switches_mode() {
let mut builder = TreeBuilder::new();
let (html, head) = bootstrap_in_head(&mut builder);
builder.process_token(&end_tag("head"), pos());
let outcome = builder.process_token_after_head(&TokenKind::Character('x'), pos());
assert!(matches!(outcome, super::TokenOutcome::Reprocess));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
let body = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(body).kind,
NodeKind::Element {
name: "body".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.document.children(html).collect::<Vec<_>>(),
vec![head, body]
);
}
fn bootstrap_in_body(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
builder.process_token(&start_tag("body"), pos());
let root = builder.document.root();
let html = builder.document.children(root).next().unwrap();
let body = builder.open_elements.current_node().unwrap();
(html, body)
}
#[test]
fn in_body_character_sets_frameset_ok_and_ignores_nul() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.frameset_ok = true;
builder.process_token(&TokenKind::Character('\0'), pos());
assert_eq!(builder.document.children(body).count(), 0);
builder.process_token(&TokenKind::Character('x'), pos());
assert!(!builder.frameset_ok);
let children: Vec<_> = builder.document.children(body).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn in_body_div_closes_an_open_p_in_button_scope() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("p"), pos());
let p = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("div"), pos());
assert!(!builder.open_elements.contains(p));
let div = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![p, div]
);
}
#[test]
fn in_body_heading_pops_a_currently_open_heading() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("h1"), pos());
let h1 = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("h2"), pos());
assert!(!builder.open_elements.contains(h1));
let h2 = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![h1, h2]
);
}
#[test]
fn in_body_formatting_start_tag_pushes_onto_active_formatting_elements() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("b"), pos());
let b = builder.open_elements.current_node().unwrap();
assert!(matches!(
builder.active_formatting_elements.entries.last(),
Some(super::FormattingEntry::Element(node)) if *node == b
));
}
#[test]
fn in_body_end_tag_b_runs_the_adoption_agency_algorithm() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("b"), pos());
let b = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("b"), pos());
assert!(!builder.open_elements.contains(b));
assert_eq!(builder.open_elements.current_node(), Some(body));
}
#[test]
fn in_body_second_a_start_tag_closes_the_first_via_adoption_agency() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("a"), pos());
let a1 = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("a"), pos());
let a2 = builder.open_elements.current_node().unwrap();
assert_ne!(a1, a2);
assert!(!builder.open_elements.contains(a1));
assert!(matches!(
builder.active_formatting_elements.entries.last(),
Some(super::FormattingEntry::Element(node)) if *node == a2
));
}
#[test]
fn in_body_void_element_is_inserted_then_immediately_popped() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("br"), pos());
let children: Vec<_> = builder.document.children(body).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "br".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.open_elements.current_node(), Some(body));
assert!(!builder.frameset_ok);
}
#[test]
fn in_body_br_end_tag_is_treated_as_a_start_tag() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&end_tag("br"), pos());
let children: Vec<_> = builder.document.children(body).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "br".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn in_body_li_closes_a_previous_open_li() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("ul"), pos());
builder.process_token(&start_tag("li"), pos());
let li1 = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("li"), pos());
assert!(!builder.open_elements.contains(li1));
let ul = builder.document.children(body).next().unwrap();
let ul_children: Vec<_> = builder.document.children(ul).collect();
assert_eq!(ul_children.len(), 2);
assert_eq!(ul_children[0], li1);
}
#[test]
fn in_body_form_sets_pointer_and_ignores_a_second_form() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("form"), pos());
let form1 = builder.open_elements.current_node().unwrap();
assert_eq!(builder.form_element_pointer, Some(form1));
builder.process_token(&start_tag("form"), pos());
assert_eq!(builder.open_elements.current_node(), Some(form1));
assert_eq!(builder.form_element_pointer, Some(form1));
}
#[test]
fn in_body_table_switches_insertion_mode() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("table"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTable);
assert!(!builder.frameset_ok);
}
#[test]
fn in_body_plaintext_returns_plaintext_state_without_switching_mode() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
let state = builder.process_token(&start_tag("plaintext"), pos());
assert_eq!(state, Some(ExternalState::PlainText));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
}
#[test]
fn in_body_textarea_switches_to_text_mode_and_skips_next_lf() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
let state = builder.process_token(&start_tag("textarea"), pos());
assert_eq!(state, Some(ExternalState::RcData));
assert_eq!(builder.insertion_mode, InsertionMode::Text);
assert!(builder.skip_next_line_feed);
assert!(!builder.frameset_ok);
let textarea = builder.open_elements.current_node().unwrap();
builder.process_token(&TokenKind::Character('\n'), pos());
assert_eq!(builder.document.children(textarea).count(), 0);
assert!(!builder.skip_next_line_feed);
builder.process_token(&TokenKind::Character('x'), pos());
assert_eq!(builder.document.children(textarea).count(), 1);
}
#[test]
fn in_body_pre_ignores_one_leading_line_feed() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("pre"), pos());
let pre = builder.open_elements.current_node().unwrap();
assert!(builder.skip_next_line_feed);
builder.process_token(&TokenKind::Character('\n'), pos());
assert_eq!(builder.document.children(pre).count(), 0);
assert!(!builder.skip_next_line_feed);
builder.process_token(&TokenKind::Character('x'), pos());
assert_eq!(builder.document.children(pre).count(), 1);
}
#[test]
fn in_body_math_inserts_with_mathml_namespace() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("math"), pos());
let math = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(math).kind,
NodeKind::Element {
name: "math".to_owned(),
namespace: Some(super::MATHML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn in_body_any_other_start_tag_is_inserted_as_an_ordinary_element() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("span"), pos());
let span = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(span).kind,
NodeKind::Element {
name: "span".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![span]
);
}
#[test]
fn in_body_stray_table_section_start_tags_are_ignored() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("tbody"), pos());
assert_eq!(builder.document.children(body).count(), 0);
assert_eq!(builder.open_elements.current_node(), Some(body));
}
#[test]
fn in_body_delegates_meta_to_in_head_rules() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("meta"), pos());
let children: Vec<_> = builder.document.children(body).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "meta".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.open_elements.current_node(), Some(body));
}
#[test]
fn in_body_any_other_end_tag_pops_through_a_matching_ancestor() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("custom-element"), pos());
let custom = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("custom-element"), pos());
assert!(!builder.open_elements.contains(custom));
assert_eq!(builder.open_elements.current_node(), Some(body));
}
#[test]
fn text_mode_inserts_characters_into_the_rcdata_element() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("title"), pos());
let title = builder.open_elements.current_node().unwrap();
assert_eq!(builder.insertion_mode, InsertionMode::Text);
builder.process_token(&TokenKind::Character('h'), pos());
builder.process_token(&TokenKind::Character('i'), pos());
let children: Vec<_> = builder.document.children(title).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: "hi".to_owned()
}
);
}
#[test]
fn text_mode_end_tag_pops_and_restores_original_insertion_mode() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("title"), pos());
let title = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("nonsense"), pos());
assert!(!builder.open_elements.contains(title));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
assert_eq!(builder.open_elements.current_node(), Some(body));
assert_eq!(builder.original_insertion_mode, None);
}
#[test]
fn text_mode_eof_pops_and_reprocesses_in_the_original_mode() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("title"), pos());
let title = builder.open_elements.current_node().unwrap();
builder.process_token(&TokenKind::Eof, pos());
assert!(!builder.open_elements.contains(title));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
assert_eq!(builder.open_elements.current_node(), Some(body));
}
fn bootstrap_in_table(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
let (_, body) = bootstrap_in_body(builder);
builder.process_token(&start_tag("table"), pos());
let table = builder.open_elements.current_node().unwrap();
(body, table)
}
#[test]
fn in_table_caption_switches_mode_and_pushes_a_marker() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("caption"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InCaption);
let caption = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(table).collect::<Vec<_>>(),
vec![caption]
);
assert!(matches!(
builder.active_formatting_elements.entries.last(),
Some(super::FormattingEntry::Marker)
));
}
#[test]
fn in_table_col_synthesizes_a_colgroup_and_reprocesses() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("col"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InColumnGroup);
let colgroup = builder.document.children(table).next().unwrap();
assert_eq!(
builder.document.node(colgroup).kind,
NodeKind::Element {
name: "colgroup".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let col = builder.document.children(colgroup).next().unwrap();
assert_eq!(
builder.document.node(col).kind,
NodeKind::Element {
name: "col".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.open_elements.current_node(), Some(colgroup));
}
#[test]
fn in_table_tr_synthesizes_tbody_then_cascades_into_in_row() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tr"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InRow);
let tbody = builder.document.children(table).next().unwrap();
assert_eq!(
builder.document.node(tbody).kind,
NodeKind::Element {
name: "tbody".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let tr = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(tbody).collect::<Vec<_>>(),
vec![tr]
);
}
#[test]
fn in_table_nested_table_closes_the_first_and_starts_a_sibling() {
let mut builder = TreeBuilder::new();
let (body, table1) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("table"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTable);
let table2 = builder.open_elements.current_node().unwrap();
assert_ne!(table1, table2);
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![table1, table2]
);
assert_eq!(builder.document.children(table1).count(), 0);
}
#[test]
fn in_table_whitespace_character_is_buffered_and_inserted_normally() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&TokenKind::Character(' '), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTableText);
builder.process_token(&start_tag("caption"), pos());
let children: Vec<_> = builder.document.children(table).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: " ".to_owned()
}
);
assert_eq!(builder.insertion_mode, InsertionMode::InCaption);
}
#[test]
fn in_table_non_whitespace_character_is_foster_parented_before_the_table() {
let mut builder = TreeBuilder::new();
let (body, table) = bootstrap_in_table(&mut builder);
builder.process_token(&TokenKind::Character('x'), pos());
builder.process_token(&start_tag("caption"), pos());
let body_children: Vec<_> = builder.document.children(body).collect();
assert_eq!(body_children.len(), 2);
assert_eq!(
builder.document.node(body_children[0]).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
assert_eq!(body_children[1], table);
assert_eq!(builder.document.children(table).count(), 1); }
#[test]
fn in_caption_end_tag_closes_and_returns_to_in_table() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("caption"), pos());
let caption = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("caption"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTable);
assert!(!builder.open_elements.contains(caption));
assert_eq!(builder.open_elements.current_node(), Some(table));
}
#[test]
fn in_column_group_non_whitespace_closes_and_reprocesses_in_table() {
let mut builder = TreeBuilder::new();
let (_, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("colgroup"), pos());
let colgroup = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("tbody"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTableBody);
assert!(!builder.open_elements.contains(colgroup));
let tbody = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.children(table).collect::<Vec<_>>(),
vec![colgroup, tbody]
);
}
#[test]
fn in_table_body_end_tag_table_closes_tbody_and_the_table() {
let mut builder = TreeBuilder::new();
let (body, table) = bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tbody"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTableBody);
builder.process_token(&end_tag("table"), pos());
assert!(!builder.open_elements.contains(table));
assert_eq!(builder.open_elements.current_node(), Some(body));
}
#[test]
fn in_row_td_switches_to_in_cell_and_pushes_a_marker() {
let mut builder = TreeBuilder::new();
bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tr"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InRow);
let tr = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("td"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InCell);
let td = builder.open_elements.current_node().unwrap();
assert_eq!(builder.document.children(tr).collect::<Vec<_>>(), vec![td]);
assert!(matches!(
builder.active_formatting_elements.entries.last(),
Some(super::FormattingEntry::Marker)
));
}
#[test]
fn in_row_end_tag_tr_returns_to_in_table_body() {
let mut builder = TreeBuilder::new();
bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tr"), pos());
let tr = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("tr"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InTableBody);
assert!(!builder.open_elements.contains(tr));
}
#[test]
fn in_cell_end_tag_td_closes_and_returns_to_in_row() {
let mut builder = TreeBuilder::new();
bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tr"), pos());
let tr = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("td"), pos());
let td = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("td"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InRow);
assert!(!builder.open_elements.contains(td));
assert_eq!(builder.open_elements.current_node(), Some(tr));
}
#[test]
fn in_cell_next_cell_start_tag_closes_the_current_one_and_reprocesses() {
let mut builder = TreeBuilder::new();
bootstrap_in_table(&mut builder);
builder.process_token(&start_tag("tr"), pos());
let tr = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("td"), pos());
let td1 = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("td"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InCell);
let td2 = builder.open_elements.current_node().unwrap();
assert_ne!(td1, td2);
assert!(!builder.open_elements.contains(td1));
assert_eq!(
builder.document.children(tr).collect::<Vec<_>>(),
vec![td1, td2]
);
}
fn bootstrap_after_body(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
let (html, body) = bootstrap_in_body(builder);
builder.process_token(&end_tag("body"), pos());
(html, body)
}
#[test]
fn after_body_comment_is_appended_to_the_html_element() {
let mut builder = TreeBuilder::new();
let (html, body) = bootstrap_after_body(&mut builder);
assert_eq!(builder.insertion_mode, InsertionMode::AfterBody);
builder.process_token(&TokenKind::Comment("hi".to_owned()), pos());
let html_children: Vec<_> = builder.document.children(html).collect();
assert_eq!(html_children.len(), 3);
assert_eq!(html_children[1], body);
assert_eq!(
builder.document.node(html_children[2]).kind,
NodeKind::Comment {
content: "hi".to_owned()
}
);
}
#[test]
fn after_body_end_tag_html_switches_to_after_after_body() {
let mut builder = TreeBuilder::new();
bootstrap_after_body(&mut builder);
builder.process_token(&end_tag("html"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterAfterBody);
}
#[test]
fn after_body_whitespace_character_is_delegated_to_in_body() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_after_body(&mut builder);
builder.process_token(&TokenKind::Character(' '), pos());
let children: Vec<_> = builder.document.children(body).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: " ".to_owned()
}
);
assert_eq!(builder.insertion_mode, InsertionMode::AfterBody);
}
#[test]
fn after_body_anything_else_switches_to_in_body_and_reprocesses() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_after_body(&mut builder);
builder.process_token(&start_tag("p"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
let p = builder.open_elements.current_node().unwrap();
assert_eq!(builder.document.children(body).collect::<Vec<_>>(), vec![p]);
}
fn bootstrap_after_after_body(builder: &mut TreeBuilder) -> NodeId {
bootstrap_after_body(builder);
builder.process_token(&end_tag("html"), pos());
builder.document.root()
}
#[test]
fn after_after_body_comment_is_appended_to_the_document() {
let mut builder = TreeBuilder::new();
let root = bootstrap_after_after_body(&mut builder);
builder.process_token(&TokenKind::Comment("hi".to_owned()), pos());
let children: Vec<_> = builder.document.children(root).collect();
let last = *children.last().unwrap();
assert_eq!(
builder.document.node(last).kind,
NodeKind::Comment {
content: "hi".to_owned()
}
);
}
#[test]
fn after_after_body_whitespace_character_is_delegated_to_in_body() {
let mut builder = TreeBuilder::new();
bootstrap_after_after_body(&mut builder);
builder.process_token(&TokenKind::Character(' '), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterAfterBody);
}
#[test]
fn after_after_body_anything_else_switches_to_in_body_and_reprocesses() {
let mut builder = TreeBuilder::new();
bootstrap_after_after_body(&mut builder);
builder.process_token(&start_tag("p"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
let p = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(p).kind,
NodeKind::Element {
name: "p".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn in_body_frameset_replaces_body_when_frameset_ok() {
let mut builder = TreeBuilder::new();
let (html, body) = bootstrap_in_body(&mut builder);
builder.frameset_ok = true;
let p = builder.insert_html_element(
&TagToken {
name: "p".to_owned(),
self_closing: false,
attributes: vec![],
},
None,
);
builder.process_token(&start_tag("frameset"), pos());
let html_children: Vec<_> = builder.document.children(html).collect();
assert_eq!(html_children.len(), 2);
let frameset = html_children[1];
assert_eq!(
builder.document.node(frameset).kind,
NodeKind::Element {
name: "frameset".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.document.parent(body), None);
assert_eq!(builder.document.parent(p), Some(body));
assert_eq!(builder.open_elements.current_node(), Some(frameset));
assert_eq!(builder.insertion_mode, InsertionMode::InFrameset);
}
#[test]
fn in_body_frameset_ignored_when_frameset_ok_is_not_ok() {
let mut builder = TreeBuilder::new();
let (_, body) = bootstrap_in_body(&mut builder);
assert!(!builder.frameset_ok);
builder.process_token(&start_tag("frameset"), pos());
assert_eq!(builder.open_elements.current_node(), Some(body));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
}
#[test]
fn after_head_frameset_inserts_and_switches_mode() {
let mut builder = TreeBuilder::new();
builder.process_token(&start_tag("head"), pos());
builder.process_token(&end_tag("head"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterHead);
builder.process_token(&start_tag("frameset"), pos());
let frameset = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(frameset).kind,
NodeKind::Element {
name: "frameset".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.insertion_mode, InsertionMode::InFrameset);
}
fn bootstrap_in_frameset(builder: &mut TreeBuilder) -> NodeId {
builder.process_token(&start_tag("head"), pos());
builder.process_token(&end_tag("head"), pos());
builder.process_token(&start_tag("frameset"), pos());
builder.open_elements.current_node().unwrap()
}
#[test]
fn in_frameset_nested_frameset_start_tag_is_inserted() {
let mut builder = TreeBuilder::new();
let outer = bootstrap_in_frameset(&mut builder);
builder.process_token(&start_tag("frameset"), pos());
let inner = builder.open_elements.current_node().unwrap();
assert_ne!(inner, outer);
assert_eq!(builder.document.parent(inner), Some(outer));
assert_eq!(builder.insertion_mode, InsertionMode::InFrameset);
}
#[test]
fn in_frameset_frame_start_tag_is_inserted_and_immediately_popped() {
let mut builder = TreeBuilder::new();
let frameset = bootstrap_in_frameset(&mut builder);
builder.process_token(&start_tag("frame"), pos());
assert_eq!(builder.open_elements.current_node(), Some(frameset));
let children: Vec<_> = builder.document.children(frameset).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Element {
name: "frame".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn in_frameset_end_tag_pops_and_switches_to_after_frameset() {
let mut builder = TreeBuilder::new();
bootstrap_in_frameset(&mut builder);
builder.process_token(&end_tag("frameset"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterFrameset);
}
#[test]
fn in_frameset_nested_end_tag_stays_in_frameset_mode() {
let mut builder = TreeBuilder::new();
let outer = bootstrap_in_frameset(&mut builder);
builder.process_token(&start_tag("frameset"), pos());
builder.process_token(&end_tag("frameset"), pos());
assert_eq!(builder.open_elements.current_node(), Some(outer));
assert_eq!(builder.insertion_mode, InsertionMode::InFrameset);
}
#[test]
fn in_frameset_whitespace_is_inserted_non_whitespace_is_ignored() {
let mut builder = TreeBuilder::new();
let frameset = bootstrap_in_frameset(&mut builder);
builder.process_token(&TokenKind::Character(' '), pos());
builder.process_token(&TokenKind::Character('x'), pos());
let children: Vec<_> = builder.document.children(frameset).collect();
assert_eq!(children.len(), 1);
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: " ".to_owned()
}
);
}
#[test]
fn after_frameset_end_tag_html_switches_to_after_after_frameset() {
let mut builder = TreeBuilder::new();
bootstrap_in_frameset(&mut builder);
builder.process_token(&end_tag("frameset"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterFrameset);
builder.process_token(&end_tag("html"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterAfterFrameset);
}
#[test]
fn after_after_frameset_comment_is_appended_to_the_document() {
let mut builder = TreeBuilder::new();
bootstrap_in_frameset(&mut builder);
builder.process_token(&end_tag("frameset"), pos());
builder.process_token(&end_tag("html"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterAfterFrameset);
let root = builder.document.root();
builder.process_token(&TokenKind::Comment("hi".to_owned()), pos());
let children: Vec<_> = builder.document.children(root).collect();
assert_eq!(
builder.document.node(*children.last().unwrap()).kind,
NodeKind::Comment {
content: "hi".to_owned()
}
);
}
#[test]
fn after_after_frameset_anything_else_is_ignored_not_reprocessed() {
let mut builder = TreeBuilder::new();
bootstrap_in_frameset(&mut builder);
builder.process_token(&end_tag("frameset"), pos());
builder.process_token(&end_tag("html"), pos());
let current_before = builder.open_elements.current_node();
let stack_len_before = builder.open_elements.entries.len();
builder.process_token(&start_tag("p"), pos());
assert_eq!(builder.insertion_mode, InsertionMode::AfterAfterFrameset);
assert_eq!(builder.open_elements.current_node(), current_before);
assert_eq!(builder.open_elements.entries.len(), stack_len_before);
}
fn bootstrap_in_svg(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
let (_, body) = bootstrap_in_body(builder);
builder.process_token(&start_tag("svg"), pos());
let svg = builder.open_elements.current_node().unwrap();
(body, svg)
}
#[test]
fn foreign_content_ordinary_svg_element_gets_svg_namespace() {
let mut builder = TreeBuilder::new();
let (_, svg) = bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("circle"), pos());
let circle = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(circle).kind,
NodeKind::Element {
name: "circle".to_owned(),
namespace: Some(super::SVG_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.document.children(svg).collect::<Vec<_>>(),
vec![circle]
);
}
#[test]
fn foreign_content_svg_tag_name_is_case_fixed() {
let mut builder = TreeBuilder::new();
bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("foreignobject"), pos());
let node = builder.open_elements.current_node().unwrap();
let NodeKind::Element { name, .. } = &builder.document.node(node).kind else {
unreachable!()
};
assert_eq!(name, "foreignObject");
}
#[test]
fn foreign_content_svg_attribute_is_case_fixed() {
let mut builder = TreeBuilder::new();
bootstrap_in_svg(&mut builder);
builder.process_token(
&start_tag_with_attrs("rect", &[("viewbox", "0 0 1 1")]),
pos(),
);
let node = builder.open_elements.current_node().unwrap();
let NodeKind::Element { attributes, .. } = &builder.document.node(node).kind else {
unreachable!()
};
assert_eq!(attributes[0].name, "viewBox");
}
#[test]
fn foreign_content_xlink_attribute_gets_namespaced() {
let mut builder = TreeBuilder::new();
bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag_with_attrs("use", &[("xlink:href", "#a")]), pos());
let node = builder.open_elements.current_node().unwrap();
let NodeKind::Element { attributes, .. } = &builder.document.node(node).kind else {
unreachable!()
};
assert_eq!(attributes[0].name, "xlink:href");
assert_eq!(
attributes[0].namespace.as_deref(),
Some(super::XLINK_NAMESPACE)
);
}
#[test]
fn math_definitionurl_attribute_is_case_fixed() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(
&start_tag_with_attrs("math", &[("definitionurl", "x")]),
pos(),
);
let math = builder.open_elements.current_node().unwrap();
let NodeKind::Element { attributes, .. } = &builder.document.node(math).kind else {
unreachable!()
};
assert_eq!(attributes[0].name, "definitionURL");
}
#[test]
fn foreign_content_html_integration_point_allows_html_rules_inside() {
let mut builder = TreeBuilder::new();
bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("foreignobject"), pos());
let foreign_object = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("div"), pos());
let div = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(div).kind,
NodeKind::Element {
name: "div".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder
.document
.children(foreign_object)
.collect::<Vec<_>>(),
vec![div]
);
}
#[test]
fn foreign_content_mathml_text_integration_point_allows_html_start_tags() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("math"), pos());
builder.process_token(&start_tag("mi"), pos());
let mi = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("b"), pos());
let b = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(b).kind,
NodeKind::Element {
name: "b".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(builder.document.children(mi).collect::<Vec<_>>(), vec![b]);
}
#[test]
fn foreign_content_mathml_text_integration_point_still_treats_mglyph_as_foreign() {
let mut builder = TreeBuilder::new();
bootstrap_in_body(&mut builder);
builder.process_token(&start_tag("math"), pos());
builder.process_token(&start_tag("mi"), pos());
builder.process_token(&start_tag("mglyph"), pos());
let mglyph = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(mglyph).kind,
NodeKind::Element {
name: "mglyph".to_owned(),
namespace: Some(super::MATHML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn foreign_content_block_element_start_tag_pops_out_and_reprocesses_as_html() {
let mut builder = TreeBuilder::new();
let (body, svg) = bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("b"), pos());
assert!(!builder.open_elements.contains(svg));
let b = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(b).kind,
NodeKind::Element {
name: "b".to_owned(),
namespace: Some(super::HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
assert_eq!(
builder.document.children(body).collect::<Vec<_>>(),
vec![svg, b]
);
}
#[test]
fn foreign_content_end_tag_p_pops_out_of_foreign_content() {
let mut builder = TreeBuilder::new();
let (_, svg) = bootstrap_in_svg(&mut builder);
builder.process_token(&end_tag("p"), pos());
assert!(!builder.open_elements.contains(svg));
assert_eq!(builder.insertion_mode, InsertionMode::InBody);
}
#[test]
fn foreign_content_nul_character_becomes_replacement_character() {
let mut builder = TreeBuilder::new();
let (_, svg) = bootstrap_in_svg(&mut builder);
builder.process_token(&TokenKind::Character('\0'), pos());
let children: Vec<_> = builder.document.children(svg).collect();
assert_eq!(
builder.document.node(children[0]).kind,
NodeKind::Text {
content: "\u{FFFD}".to_owned()
}
);
}
#[test]
fn foreign_content_any_other_end_tag_pops_through_a_matching_element() {
let mut builder = TreeBuilder::new();
let (_, svg) = bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("circle"), pos());
let circle = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("circle"), pos());
assert!(!builder.open_elements.contains(circle));
assert_eq!(builder.open_elements.current_node(), Some(svg));
}
#[test]
fn foreign_content_math_inside_svg_keeps_the_svg_namespace() {
let mut builder = TreeBuilder::new();
bootstrap_in_svg(&mut builder);
builder.process_token(&start_tag("math"), pos());
let math = builder.open_elements.current_node().unwrap();
assert_eq!(
builder.document.node(math).kind,
NodeKind::Element {
name: "math".to_owned(),
namespace: Some(super::SVG_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
fn bootstrap_select_with_selectedcontent(builder: &mut TreeBuilder) -> (NodeId, NodeId) {
bootstrap_in_body(builder);
builder.process_token(&start_tag("select"), pos());
let select = builder.open_elements.current_node().unwrap();
builder.process_token(&start_tag("button"), pos());
builder.process_token(&start_tag("selectedcontent"), pos());
let selectedcontent = builder.open_elements.current_node().unwrap();
builder.process_token(&end_tag("button"), pos());
(select, selectedcontent)
}
fn text_content_of(builder: &TreeBuilder, node: NodeId) -> String {
builder
.document
.children(node)
.map(|child| match &builder.document.node(child).kind {
NodeKind::Text { content } => content.clone(),
_ => String::new(),
})
.collect()
}
#[test]
fn second_option_start_tag_clones_the_default_selected_first_option() {
let mut builder = TreeBuilder::new();
let (_, selectedcontent) = bootstrap_select_with_selectedcontent(&mut builder);
builder.process_token(&start_tag("option"), pos());
builder.process_token(&TokenKind::Character('X'), pos());
builder.process_token(&start_tag("option"), pos());
builder.process_token(&TokenKind::Character('Y'), pos());
assert_eq!(text_content_of(&builder, selectedcontent), "X");
}
#[test]
fn stop_parsing_reclones_a_later_option_that_becomes_selected() {
let mut builder = TreeBuilder::new();
let (_, selectedcontent) = bootstrap_select_with_selectedcontent(&mut builder);
builder.process_token(&start_tag("option"), pos());
builder.process_token(&TokenKind::Character('X'), pos());
builder.process_token(&start_tag_with_attrs("option", &[("selected", "")]), pos());
builder.process_token(&TokenKind::Character('Y'), pos());
assert_eq!(text_content_of(&builder, selectedcontent), "X");
builder.stop_parsing();
assert_eq!(text_content_of(&builder, selectedcontent), "Y");
}
#[test]
fn selectedcontent_clone_is_a_deep_copy_not_a_reference() {
let mut builder = TreeBuilder::new();
let (_, selectedcontent) = bootstrap_select_with_selectedcontent(&mut builder);
builder.process_token(&start_tag("option"), pos());
let option = builder.open_elements.current_node().unwrap();
builder.process_token(&TokenKind::Character('X'), pos());
builder.stop_parsing();
let cloned_text = builder.document.children(selectedcontent).next().unwrap();
let original_text = builder.document.children(option).next().unwrap();
assert_ne!(cloned_text, original_text);
assert_eq!(
builder.document.node(cloned_text).kind,
builder.document.node(original_text).kind
);
}
}