mod document;
mod entities;
mod tokenizer;
mod tree_builder;
pub use document::{Attribute, Children, Document, Node, NodeId, NodeKind};
pub use tokenizer::{ParseError, ParseErrorKind, Position};
use tokenizer::Tokenizer;
use tree_builder::TreeBuilder;
#[derive(Debug)]
pub struct ParseResult {
pub document: Document,
pub errors: Vec<ParseError>,
}
pub fn parse(input: &str) -> ParseResult {
let mut tokenizer = Tokenizer::new(input);
let mut tree_builder = TreeBuilder::new();
while let Some(token) = tokenizer.next() {
if let Some(state) = tree_builder.process_token(&token.kind, token.position) {
tokenizer.switch_to(state);
}
tokenizer.set_in_foreign_content(tree_builder.is_in_foreign_content());
}
tree_builder.stop_parsing();
let mut errors = tokenizer.take_errors();
errors.append(&mut tree_builder.take_errors());
errors.sort_by_key(|error| error.position.byte_offset);
ParseResult {
document: tree_builder.into_document(),
errors,
}
}
#[cfg(test)]
mod tests {
use super::parse;
use crate::document::{Document, NodeId, NodeKind};
use crate::tree_builder::{HTML_NAMESPACE, MATHML_NAMESPACE, SVG_NAMESPACE};
fn body_of(document: &Document) -> NodeId {
let root = document.root();
let html = document
.children(root)
.find(|&node| matches!(document.node(node).kind, NodeKind::Element { .. }))
.unwrap();
document.children(html).nth(1).unwrap()
}
#[test]
fn parses_a_minimal_document_into_the_expected_tree_shape() {
let document = parse(
"<!DOCTYPE html><html><head><title>Hi</title></head><body><p>Hello</p></body></html>",
)
.document;
let root = document.root();
let root_children: Vec<_> = document.children(root).collect();
assert_eq!(root_children.len(), 2);
assert_eq!(
document.node(root_children[0]).kind,
NodeKind::Doctype {
name: Some("html".to_owned()),
public_identifier: Some(String::new()),
system_identifier: Some(String::new()),
}
);
let html = root_children[1];
let html_children: Vec<_> = document.children(html).collect();
assert_eq!(html_children.len(), 2);
let (head, body) = (html_children[0], html_children[1]);
let title = document.children(head).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(title).kind else {
unreachable!()
};
assert_eq!(name, "title");
let title_text = document.children(title).next().unwrap();
assert_eq!(
document.node(title_text).kind,
NodeKind::Text {
content: "Hi".to_owned()
}
);
let p = document.children(body).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(p).kind else {
unreachable!()
};
assert_eq!(name, "p");
let p_text = document.children(p).next().unwrap();
assert_eq!(
document.node(p_text).kind,
NodeKind::Text {
content: "Hello".to_owned()
}
);
}
#[test]
fn parses_implied_html_head_body_when_missing() {
let document = parse("<p>Hello</p>").document;
let root = document.root();
assert_eq!(document.children(root).count(), 1);
let html = document.children(root).next().unwrap();
let html_children: Vec<_> = document.children(html).collect();
assert_eq!(html_children.len(), 2);
let body = html_children[1];
let p = document.children(body).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(p).kind else {
unreachable!()
};
assert_eq!(name, "p");
}
#[test]
fn rcdata_element_content_is_not_parsed_as_markup() {
let document = parse("<title><b>not bold</b></title>").document;
let root = document.root();
let html = document.children(root).next().unwrap();
let head = document.children(html).next().unwrap();
let title = document.children(head).next().unwrap();
let text = document.children(title).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "<b>not bold</b>".to_owned()
}
);
}
#[test]
fn parse_syncs_in_foreign_content_for_cdata_sections() {
let document = parse("<svg><![CDATA[hello]]></svg>").document;
let root = document.root();
let html = document.children(root).next().unwrap();
let body = document.children(html).nth(1).unwrap();
let svg = document.children(body).next().unwrap();
let content = document.children(svg).next().unwrap();
assert_eq!(
document.node(content).kind,
NodeKind::Text {
content: "hello".to_owned()
}
);
}
#[test]
fn cdata_outside_foreign_content_becomes_a_bogus_comment() {
let document = parse("<p><![CDATA[hello]]></p>").document;
let root = document.root();
let html = document.children(root).next().unwrap();
let body = document.children(html).nth(1).unwrap();
let p = document.children(body).next().unwrap();
let content = document.children(p).next().unwrap();
assert_eq!(
document.node(content).kind,
NodeKind::Comment {
content: "[CDATA[hello]]".to_owned()
}
);
}
#[test]
fn optional_end_tags_produce_sibling_li_elements() {
let document = parse("<ul><li>a<li>b</ul>").document;
let body = body_of(&document);
let ul = document.children(body).next().unwrap();
let items: Vec<_> = document.children(ul).collect();
assert_eq!(items.len(), 2);
for (&li, expected_text) in items.iter().zip(["a", "b"]) {
let NodeKind::Element { name, .. } = &document.node(li).kind else {
unreachable!()
};
assert_eq!(name, "li");
let text = document.children(li).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: expected_text.to_owned()
}
);
}
}
#[test]
fn svg_element_keeps_svg_namespace_end_to_end() {
let document = parse("<svg><circle/></svg>").document;
let body = body_of(&document);
let svg = document.children(body).next().unwrap();
assert_eq!(
document.node(svg).kind,
NodeKind::Element {
name: "svg".to_owned(),
namespace: Some(SVG_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let circle = document.children(svg).next().unwrap();
assert_eq!(
document.node(circle).kind,
NodeKind::Element {
name: "circle".to_owned(),
namespace: Some(SVG_NAMESPACE.to_owned()),
attributes: vec![],
}
);
}
#[test]
fn mathml_element_keeps_mathml_namespace_end_to_end() {
let document = parse("<math><mi>x</mi></math>").document;
let body = body_of(&document);
let math = document.children(body).next().unwrap();
assert_eq!(
document.node(math).kind,
NodeKind::Element {
name: "math".to_owned(),
namespace: Some(MATHML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let mi = document.children(math).next().unwrap();
assert_eq!(
document.node(mi).kind,
NodeKind::Element {
name: "mi".to_owned(),
namespace: Some(MATHML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let text = document.children(mi).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn script_content_is_not_tokenized_as_markup() {
let document = parse("<script>1 < 2;</script>").document;
let root = document.root();
let html = document.children(root).next().unwrap();
let head = document.children(html).next().unwrap();
let script = document.children(head).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(script).kind else {
unreachable!()
};
assert_eq!(name, "script");
let text = document.children(script).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "1 < 2;".to_owned()
}
);
}
#[test]
fn style_content_is_not_tokenized_as_markup() {
let document = parse("<style>a{color:red}</style>").document;
let root = document.root();
let html = document.children(root).next().unwrap();
let head = document.children(html).next().unwrap();
let style = document.children(head).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(style).kind else {
unreachable!()
};
assert_eq!(name, "style");
let text = document.children(style).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "a{color:red}".to_owned()
}
);
}
#[test]
fn named_character_references_resolve_to_decoded_text() {
let document = parse("<p>& ©</p>").document;
let body = body_of(&document);
let p = document.children(body).next().unwrap();
let text = document.children(p).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "& \u{a9}".to_owned()
}
);
}
#[test]
fn custom_element_gets_html_namespace_like_any_plain_element() {
let document = parse("<my-widget>hi</my-widget>").document;
let body = body_of(&document);
let widget = document.children(body).next().unwrap();
assert_eq!(
document.node(widget).kind,
NodeKind::Element {
name: "my-widget".to_owned(),
namespace: Some(HTML_NAMESPACE.to_owned()),
attributes: vec![],
}
);
let text = document.children(widget).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "hi".to_owned()
}
);
}
#[test]
fn xml_lang_attribute_stays_a_literal_unnamespaced_attribute_name() {
let document = parse(r#"<p xml:lang="de">hi</p>"#).document;
let body = body_of(&document);
let p = document.children(body).next().unwrap();
let NodeKind::Element { attributes, .. } = &document.node(p).kind else {
unreachable!()
};
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].name, "xml:lang");
assert_eq!(attributes[0].value, "de");
assert_eq!(attributes[0].namespace, None);
}
#[test]
fn table_without_tbody_or_tr_gets_them_synthesized() {
let document = parse("<table><td>x</td></table>").document;
let body = body_of(&document);
let table = document.children(body).next().unwrap();
let tbody = document.children(table).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(tbody).kind else {
unreachable!()
};
assert_eq!(name, "tbody");
let tr = document.children(tbody).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(tr).kind else {
unreachable!()
};
assert_eq!(name, "tr");
let td = document.children(tr).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(td).kind else {
unreachable!()
};
assert_eq!(name, "td");
let text = document.children(td).next().unwrap();
assert_eq!(
document.node(text).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn adoption_agency_spec_example_misnested_b_i_tags() {
let document = parse("<p>1<b>2<i>3</b>4</i>5</p>").document;
let body = body_of(&document);
let p = document.children(body).next().unwrap();
let p_children: Vec<_> = document.children(p).collect();
assert_eq!(p_children.len(), 4);
assert_eq!(
document.node(p_children[0]).kind,
NodeKind::Text {
content: "1".to_owned()
}
);
let b = p_children[1];
let NodeKind::Element { name, .. } = &document.node(b).kind else {
unreachable!()
};
assert_eq!(name, "b");
let b_children: Vec<_> = document.children(b).collect();
assert_eq!(b_children.len(), 2);
assert_eq!(
document.node(b_children[0]).kind,
NodeKind::Text {
content: "2".to_owned()
}
);
let inner_i = b_children[1];
let NodeKind::Element { name, .. } = &document.node(inner_i).kind else {
unreachable!()
};
assert_eq!(name, "i");
let inner_i_text = document.children(inner_i).next().unwrap();
assert_eq!(
document.node(inner_i_text).kind,
NodeKind::Text {
content: "3".to_owned()
}
);
let outer_i = p_children[2];
let NodeKind::Element { name, .. } = &document.node(outer_i).kind else {
unreachable!()
};
assert_eq!(name, "i");
let outer_i_text = document.children(outer_i).next().unwrap();
assert_eq!(
document.node(outer_i_text).kind,
NodeKind::Text {
content: "4".to_owned()
}
);
assert_eq!(
document.node(p_children[3]).kind,
NodeKind::Text {
content: "5".to_owned()
}
);
}
#[test]
fn adoption_agency_spec_example_indirectly_nests_two_a_elements_via_table_misnesting() {
let document = parse(r#"<a href="a">a<table><a href="b">b</table>x"#).document;
let body = body_of(&document);
let body_children: Vec<_> = document.children(body).collect();
assert_eq!(body_children.len(), 2);
let a1 = body_children[0];
let a1_children: Vec<_> = document.children(a1).collect();
assert_eq!(a1_children.len(), 3);
assert_eq!(
document.node(a1_children[0]).kind,
NodeKind::Text {
content: "a".to_owned()
}
);
let a2 = a1_children[1];
let NodeKind::Element { name, .. } = &document.node(a2).kind else {
unreachable!()
};
assert_eq!(name, "a");
let a2_text = document.children(a2).next().unwrap();
assert_eq!(
document.node(a2_text).kind,
NodeKind::Text {
content: "b".to_owned()
}
);
let table = a1_children[2];
let NodeKind::Element { name, .. } = &document.node(table).kind else {
unreachable!()
};
assert_eq!(name, "table");
assert_eq!(document.children(table).count(), 0);
let a3 = body_children[1];
let NodeKind::Element { name, .. } = &document.node(a3).kind else {
unreachable!()
};
assert_eq!(name, "a");
let a3_text = document.children(a3).next().unwrap();
assert_eq!(
document.node(a3_text).kind,
NodeKind::Text {
content: "x".to_owned()
}
);
}
#[test]
fn quirks_mode_changes_whether_table_closes_an_open_p_element() {
let no_quirks = parse("<!DOCTYPE html><p><table></table>").document;
let body = body_of(&no_quirks);
let children: Vec<_> = no_quirks.children(body).collect();
assert_eq!(children.len(), 2);
let NodeKind::Element { name, .. } = &no_quirks.node(children[0]).kind else {
unreachable!()
};
assert_eq!(name, "p");
assert_eq!(no_quirks.children(children[0]).count(), 0);
let NodeKind::Element { name, .. } = &no_quirks.node(children[1]).kind else {
unreachable!()
};
assert_eq!(name, "table");
let quirks = parse("<p><table></table>").document; let body = body_of(&quirks);
let children: Vec<_> = quirks.children(body).collect();
assert_eq!(children.len(), 1);
let NodeKind::Element { name, .. } = &quirks.node(children[0]).kind else {
unreachable!()
};
assert_eq!(name, "p");
let p_children: Vec<_> = quirks.children(children[0]).collect();
assert_eq!(p_children.len(), 1);
let NodeKind::Element { name, .. } = &quirks.node(p_children[0]).kind else {
unreachable!()
};
assert_eq!(name, "table");
}
#[test]
fn end_tag_that_walks_out_of_foreign_content_does_not_loop_forever() {
let document = parse("<a><svg></a>").document;
let body = body_of(&document);
assert_eq!(document.children(body).count(), 1);
}
#[test]
fn template_end_tag_resets_a_stale_insertion_mode() {
let document = parse("<table><thead><template><td></template></table>").document;
let body = body_of(&document);
assert_eq!(document.children(body).count(), 1);
}
#[test]
fn frameset_document_replaces_body_and_ignores_stray_text() {
let document = parse("<!DOCTYPE html><frameset>test").document;
let root = document.root();
let root_children: Vec<_> = document.children(root).collect();
assert_eq!(root_children.len(), 2);
assert_eq!(
document.node(root_children[0]).kind,
NodeKind::Doctype {
name: Some("html".to_owned()),
public_identifier: Some(String::new()),
system_identifier: Some(String::new()),
}
);
let html = root_children[1];
let html_children: Vec<_> = document.children(html).collect();
assert_eq!(html_children.len(), 2);
let NodeKind::Element { name, .. } = &document.node(html_children[0]).kind else {
unreachable!()
};
assert_eq!(name, "head");
let frameset = html_children[1];
let NodeKind::Element { name, .. } = &document.node(frameset).kind else {
unreachable!()
};
assert_eq!(name, "frameset");
assert_eq!(document.children(frameset).count(), 0);
}
#[test]
fn template_content_is_a_separate_fragment_from_the_template_element() {
let document = parse("<body><template>Hello</template>").document;
let body = body_of(&document);
let template = document.children(body).next().unwrap();
let NodeKind::Element { name, .. } = &document.node(template).kind else {
unreachable!()
};
assert_eq!(name, "template");
let template_children: Vec<_> = document.children(template).collect();
assert_eq!(template_children.len(), 1);
let content = template_children[0];
assert_eq!(document.node(content).kind, NodeKind::DocumentFragment);
let content_children: Vec<_> = document.children(content).collect();
assert_eq!(content_children.len(), 1);
assert_eq!(
document.node(content_children[0]).kind,
NodeKind::Text {
content: "Hello".to_owned()
}
);
}
#[test]
fn selected_option_content_is_mirrored_into_selectedcontent() {
let document =
parse("<select><button><selectedcontent></button><option>X<option selected>Y").document;
let body = body_of(&document);
let select = document.children(body).next().unwrap();
let button = document.children(select).next().unwrap();
let selectedcontent = document.children(button).next().unwrap();
let selectedcontent_children: Vec<_> = document.children(selectedcontent).collect();
assert_eq!(selectedcontent_children.len(), 1);
assert_eq!(
document.node(selectedcontent_children[0]).kind,
NodeKind::Text {
content: "Y".to_owned()
}
);
}
}
#[cfg(test)]
mod tree_construction_error_tests {
use super::parse;
use crate::tokenizer::ParseErrorKind;
fn kinds(input: &str) -> Vec<ParseErrorKind> {
parse(input)
.errors
.into_iter()
.map(|error| error.kind)
.collect()
}
#[track_caller]
fn assert_raises(input: &str, expected: ParseErrorKind) {
let raised = kinds(input);
assert!(
raised.contains(&expected),
"expected {expected:?} for {input:?}, got {raised:?}"
);
}
#[track_caller]
fn assert_does_not_raise(input: &str, unexpected: ParseErrorKind) {
let raised = kinds(input);
assert!(
!raised.contains(&unexpected),
"expected no {unexpected:?} for {input:?}, got {raised:?}"
);
}
#[test]
fn implied_p_end_tag_with_unclosed_elements() {
assert_raises(
"<!doctype html><p><span><div>",
ParseErrorKind::ImpliedEndTagWithUnclosedElements,
);
assert_does_not_raise(
"<!doctype html><p>text<div>",
ParseErrorKind::ImpliedEndTagWithUnclosedElements,
);
}
#[test]
fn p_end_tag_without_p_in_button_scope() {
assert_raises(
"<!doctype html><body></p>",
ParseErrorKind::EndTagPWithoutPInButtonScope,
);
assert_does_not_raise(
"<!doctype html><p>text</p>",
ParseErrorKind::EndTagPWithoutPInButtonScope,
);
}
#[test]
fn stray_end_tag_with_no_matching_open_element() {
assert_raises("<!doctype html><body></span>", ParseErrorKind::StrayEndTag);
assert_does_not_raise("<!doctype html><span>x</span>", ParseErrorKind::StrayEndTag);
}
#[test]
fn end_tag_br() {
assert_raises("<!doctype html></br>", ParseErrorKind::EndTagBr);
}
#[test]
fn self_closing_syntax_on_a_non_void_element() {
assert_raises(
"<!doctype html><div/></div>",
ParseErrorKind::NonVoidHtmlElementStartTagWithTrailingSolidus,
);
assert_does_not_raise(
"<!doctype html><br/>",
ParseErrorKind::NonVoidHtmlElementStartTagWithTrailingSolidus,
);
assert_does_not_raise(
"<!doctype html><svg><rect/></svg>",
ParseErrorKind::NonVoidHtmlElementStartTagWithTrailingSolidus,
);
}
#[test]
fn eof_with_unclosed_elements() {
assert_raises(
"<!doctype html><div>",
ParseErrorKind::EofWithUnclosedElements,
);
assert_does_not_raise(
"<!doctype html><p>text",
ParseErrorKind::EofWithUnclosedElements,
);
}
#[test]
fn eof_in_text_mode() {
assert_raises(
"<!doctype html><script>var x = 1;",
ParseErrorKind::EofInTextMode,
);
assert_does_not_raise(
"<!doctype html><script>var x = 1;</script>",
ParseErrorKind::EofInTextMode,
);
}
#[test]
fn start_tag_image() {
assert_raises(
"<!doctype html><image src=x>",
ParseErrorKind::StartTagImage,
);
assert_does_not_raise("<!doctype html><img src=x>", ParseErrorKind::StartTagImage);
}
#[test]
fn nested_form() {
assert_raises("<!doctype html><form><form>", ParseErrorKind::NestedForm);
assert_does_not_raise(
"<!doctype html><form></form><form>",
ParseErrorKind::NestedForm,
);
}
#[test]
fn start_tag_table_while_a_table_is_open() {
assert_raises(
"<!doctype html><table><table></table></table>",
ParseErrorKind::StartTagTableInTable,
);
assert_does_not_raise(
"<!doctype html><table></table><table></table>",
ParseErrorKind::StartTagTableInTable,
);
}
#[test]
fn misplaced_token_in_table() {
assert_raises(
"<!doctype html><table><select></select></table>",
ParseErrorKind::MisplacedTokenInTable,
);
assert_raises(
"<!doctype html><table><input></table>",
ParseErrorKind::MisplacedTokenInTable,
);
assert_does_not_raise(
"<!doctype html><table><tr><td>x</td></tr></table>",
ParseErrorKind::MisplacedTokenInTable,
);
}
#[test]
fn non_space_characters_in_table() {
let raised = kinds("<!doctype html><table>text</table>");
assert_eq!(
raised
.iter()
.filter(|kind| **kind == ParseErrorKind::NonSpaceCharactersInTable)
.count(),
1,
"got {raised:?}"
);
assert_does_not_raise(
"<!doctype html><table> </table>",
ParseErrorKind::NonSpaceCharactersInTable,
);
}
#[test]
fn stray_end_tag_in_table() {
assert_raises(
"<!doctype html><table></tr></table>",
ParseErrorKind::StrayEndTagInTable,
);
}
#[test]
fn token_after_body() {
assert_raises(
"<!doctype html><body></body>text",
ParseErrorKind::TokenAfterBody,
);
assert_raises(
"<!doctype html><body></body><p>x</p>",
ParseErrorKind::TokenAfterBody,
);
assert_does_not_raise(
"<!doctype html><body></body>\n",
ParseErrorKind::TokenAfterBody,
);
}
#[test]
fn stray_doctype() {
assert_raises(
"<!doctype html><title>t</title><!doctype html>",
ParseErrorKind::StrayDoctype,
);
assert_raises(
"<!doctype html><body>x<!doctype html>",
ParseErrorKind::StrayDoctype,
);
assert_does_not_raise(
"<!doctype html><title>t</title>",
ParseErrorKind::StrayDoctype,
);
}
#[test]
fn valid_document_raises_no_errors() {
let raised = kinds(
"<!doctype html><html lang=en><head><title>t</title></head><body>\
<h1>x</h1><ul><li>a</li></ul><dl><dt>t</dt><dd>d</dd></dl>\
<form><p><b><i>x</i></b> <a href=/>y</a></p></form>\
<table><caption>c</caption><colgroup><col></colgroup>\
<thead><tr><th>h</th></tr></thead><tbody><tr><td>x</td></tr></tbody></table>\
<template><div></div></template><select><option>o</option></select>\
</body></html>\n",
);
assert!(raised.is_empty(), "got {raised:?}");
}
#[test]
fn stray_end_tags_with_no_element_in_scope() {
for input in [
"<!doctype html><p>x</p></div>",
"<!doctype html><p>x</p></header>",
"<!doctype html><p>x</p></li>",
"<!doctype html><p>x</p></dd>",
"<!doctype html><p>x</p></h2>",
"<!doctype html><p>x</p></form>",
"<!doctype html><p>x</p></object>",
"<!doctype html><object></body></object>",
"<!doctype html><object></html></object>",
] {
assert_raises(input, ParseErrorKind::StrayEndTag);
}
assert_does_not_raise("<!doctype html><h1>x</h2>", ParseErrorKind::StrayEndTag);
assert_does_not_raise(
"<!doctype html><ul><li>x</li></ul>",
ParseErrorKind::StrayEndTag,
);
}
#[test]
fn stray_end_tags_before_body() {
for input in [
"<!doctype html></p>",
"<!doctype html><html></div>",
"<!doctype html><head></div></head>",
"<!doctype html><head></template></head>",
"<!doctype html><head></head></div>",
"<!doctype html><template></div></template>",
] {
assert_raises(input, ParseErrorKind::StrayEndTag);
}
assert_does_not_raise(
"<!doctype html><html><head></head><body></body></html>",
ParseErrorKind::StrayEndTag,
);
}
#[test]
fn stray_start_tag() {
for input in [
"<!doctype html><body><html lang=en>",
"<!doctype html><head><html>",
"<!doctype html><body><body>",
"<!doctype html><body><frameset>",
"<!doctype html><head><head>",
"<!doctype html><head></head><head>",
"<!doctype html><body><td>x",
"<!doctype html><body><tr>",
"<!doctype html><select><select>",
] {
assert_raises(input, ParseErrorKind::StrayStartTag);
}
assert_does_not_raise(
"<!doctype html><html><head></head><body><table><tr><td>x</td></tr></table>",
ParseErrorKind::StrayStartTag,
);
}
#[test]
fn nested_formatting_element() {
assert_raises(
"<!doctype html><a href=x><a href=y>",
ParseErrorKind::NestedFormattingElement,
);
assert_raises(
"<!doctype html><nobr><nobr>",
ParseErrorKind::NestedFormattingElement,
);
assert_does_not_raise(
"<!doctype html><a href=x>x</a><a href=y>y</a>",
ParseErrorKind::NestedFormattingElement,
);
}
#[test]
fn misnested_formatting_element() {
let input = "<!doctype html><p><b><i>x</b></i></p>";
assert_raises(input, ParseErrorKind::MisnestedFormattingElement);
assert_raises(input, ParseErrorKind::StrayEndTag);
assert_does_not_raise(
"<!doctype html><p><b><i>x</i></b></p>",
ParseErrorKind::MisnestedFormattingElement,
);
}
#[test]
fn formatting_element_not_in_scope() {
assert_raises(
"<!doctype html><b><table></b></table>",
ParseErrorKind::FormattingElementNotInScope,
);
assert_does_not_raise(
"<!doctype html><b>x</b>",
ParseErrorKind::FormattingElementNotInScope,
);
}
#[test]
fn stray_end_tags_in_table_modes() {
for input in [
"<!doctype html><table><caption></td></caption></table>",
"<!doctype html><table><colgroup></col></colgroup></table>",
"<!doctype html><table><tbody></tr></tbody></table>",
"<!doctype html><table><tbody></thead></tbody></table>",
"<!doctype html><table><tr></td></tr></table>",
"<!doctype html><table><tr><td></caption></td></tr></table>",
"<!doctype html><table><tr><td></thead></td></tr></table>",
] {
assert_raises(input, ParseErrorKind::StrayEndTagInTable);
}
assert_raises(
"<!doctype html><table><form></table>",
ParseErrorKind::MisplacedTokenInTable,
);
}
#[test]
fn stray_doctype_in_foreign_content() {
assert_raises(
"<!doctype html><svg><!doctype html></svg>",
ParseErrorKind::StrayDoctype,
);
}
#[test]
fn errors_from_both_stages_are_merged_in_document_order() {
let errors = parse("<!doctype html><p>¬AnEntity;<span><div></p>").errors;
assert!(
errors.len() >= 2,
"expected both a tokenizer and a tree-construction error, got {errors:?}"
);
assert!(
errors
.windows(2)
.all(|pair| pair[0].position.byte_offset <= pair[1].position.byte_offset),
"not in document order: {errors:?}"
);
}
}