use alloc::vec::Vec;
use vize_l0::{Allocator, SourceRoot, String};
use super::{DocumentHtmlRefusal as Refusal, NativeDocument};
use crate::markup::document::DocumentLexicalRefusal;
const CORPUS: &str = include_str!("../../../../tests/fixtures/document/html-structure.tsv");
fn root(source: &str) -> SourceRoot<'_> {
SourceRoot::new(source).expect("original small document")
}
fn source(body: &str) -> String {
let mut source = String::from("<!DOCTYPE html><html><head></head><body>");
source.push_str(body);
source.push_str("</body></html>");
source
}
#[test]
fn original_element_ancestry_matches_the_real_browser_census() {
let mut count = 0;
for row in CORPUS.lines().filter(|line| !line.starts_with('#')) {
let columns: Vec<_> = row.split('\t').collect();
assert_eq!(columns.len(), 3);
let label = columns.first().expect("original case label");
let original = columns.get(1).expect("original source");
let census = columns.get(2).expect("original browser census");
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(original));
let tree = owner.html_structure().expect(label);
let elements: Vec<_> = tree.elements().collect();
let actual: Vec<_> = elements
.iter()
.map(|element| {
assert!(core::ptr::eq(element.owner(), &owner));
let parent = element.parent().map(|parent| {
elements
.iter()
.position(|node| node.opening_span() == parent.opening_span())
.expect("original parent")
});
(element.name(), parent)
})
.collect();
let expected: Vec<_> = census
.split(',')
.map(|entry| {
let (name, parent) = entry.split_once(':').expect("real browser census entry");
(
name,
(parent != "-")
.then(|| parent.parse::<usize>().expect("original parent order")),
)
})
.collect();
assert_eq!(actual, expected, "{label}");
count += 1;
}
assert_eq!(count, 12);
}
#[test]
fn nonvoid_slash_really_keeps_the_original_html_parent_open() {
let source = source("<DIV/><SPAN></span></dIv ><br/><span></span>");
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
let tree = owner.html_structure().expect("explicit complete HTML");
let elements: Vec<_> = tree.elements().collect();
let div = elements.get(3).expect("original div");
assert_eq!(div.name(), "div");
assert_eq!(div.authored_name(), "DIV");
assert_eq!(div.opening(), "<DIV/>");
assert!(div.ignored_self_closing_slash());
let close = div.closing_span().expect("original matching close");
assert_eq!(close.slice(&source), "</dIv >");
assert_eq!(
div.children().map(|child| child.name()).collect::<Vec<_>>(),
["span"]
);
assert_eq!(
elements
.get(4)
.expect("original span")
.parent()
.expect("actual div parent")
.opening_span(),
div.opening_span()
);
let br = elements.get(5).expect("original void br");
assert_eq!(br.name(), "br");
assert!(!br.ignored_self_closing_slash());
assert_eq!(br.closing_span(), None);
assert_eq!(br.parent().expect("void sibling parent").name(), "body");
}
#[test]
fn moving_the_original_owner_keeps_source_and_arena_custody() {
let source = source("<div><span></span></div>");
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
let moved = owner;
let tree = moved.html_structure().expect("original moved owner");
assert!(core::ptr::eq(tree.owner(), &moved));
assert!(core::ptr::eq(tree.owner().allocator, &allocator));
let div = tree.elements().nth(3).expect("original div");
assert!(core::ptr::eq(div.owner(), &moved));
let expected = source.find("<div>").expect("authored div");
assert!(core::ptr::eq(
div.opening().as_ptr(),
source
.get(expected..)
.expect("original div suffix")
.as_ptr()
));
}
#[test]
fn implicit_envelopes_and_text_insertion_modes_refuse_completion() {
for source in [
"<DIV/><SPAN></SPAN></DIV>",
"<!DOCTYPE html><html><body></body></html>",
" <!DOCTYPE html><html><head></head><body></body></html>",
"<!DOCTYPE html><html><head>  </head><body></body></html>",
"<!DOCTYPE html><html><head>nonspace</head><body></body></html>",
"<!DOCTYPE html><html><head></head><body></body>tail</html>",
"<!DOCTYPE html><html><head></head><body></body></html>\u{a0}",
] {
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(source));
assert_eq!(
owner.html_structure().unwrap_err(),
Refusal::ExplicitEnvelope,
"{source}"
);
}
}
#[test]
fn decoded_nonspace_entities_cannot_hide_implicit_envelope_changes() {
for source in [
" <!DOCTYPE html><html><head></head><body></body></html>",
"<!DOCTYPE html>  <html><head></head><body></body></html>",
"<!DOCTYPE html><html> <head></head><body></body></html>",
"<!DOCTYPE html><html><head>≂̸</head><body></body></html>",
"<!DOCTYPE html><html><head></head>&lt;<body></body></html>",
"<!DOCTYPE html><html><head></head><body></body><div></html>",
"<!DOCTYPE html><html><head></head><body></body></html>€",
"	x<!DOCTYPE html><html><head></head><body></body></html>",
] {
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(source));
assert!(owner.normal_completion().is_ok(), "{source}");
assert_eq!(
owner.html_structure().unwrap_err(),
Refusal::ExplicitEnvelope,
"{source}"
);
assert_eq!(owner.source(), source);
}
}
#[test]
fn vue_flavored_callbacks_cannot_hide_real_html_structure() {
for (body, refusal) in [
("{{ '<span>' }}", Refusal::VueSyntax),
(
"<div :[a>b]=x></div>",
Refusal::Lexical(DocumentLexicalRefusal::RecoveredSyntax),
),
("<div v-scope=state></div>", Refusal::VueSyntax),
] {
let source = source(body);
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
assert_eq!(owner.html_structure().unwrap_err(), refusal, "{body}");
}
}
#[test]
fn foreign_raw_formatting_and_select_content_are_explicit() {
for body in [
"<svg></svg>",
"<math></math>",
"<script></script>",
"<SCRIPT></SCRIPT>",
"<b></b>",
"<template></template>",
"<select><option></option></select>",
] {
let source = source(body);
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
assert!(
matches!(owner.html_structure(), Err(Refusal::UnsupportedElement(_))),
"{body}"
);
}
}
#[test]
fn original_mismatched_ends_and_non_html_close_frames_cannot_mint_a_tree() {
for body in ["<div><span></div></span>", "<div/>", "<br></br>"] {
let source = source(body);
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
assert!(
matches!(owner.html_structure(), Err(Refusal::ImpliedEnd(_))),
"{body}"
);
}
for body in ["<div></ div>", "<div/ ></div>"] {
let source = source(body);
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(&source));
assert!(
matches!(owner.html_structure(), Err(Refusal::InvalidFrame(_))),
"{body}"
);
}
}
#[test]
fn lexical_recovery_and_pending_structure_never_become_html_completion() {
for source in ["<!>", "<div a=\"", "{{"] {
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(source));
assert!(
matches!(owner.html_structure(), Err(Refusal::Lexical(_))),
"{source}"
);
}
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(
&allocator,
root("<!DOCTYPE html><html><head></head><body><div>"),
);
assert_eq!(
owner.html_structure().unwrap_err(),
Refusal::PendingStructure
);
assert!(!matches!(
owner.html_structure(),
Err(Refusal::Lexical(DocumentLexicalRefusal::PendingSyntax))
));
}
#[test]
fn the_original_pinned_petite_vue_document_still_reports_unsupported_structure() {
let source = include_str!("../../../../tests/fixtures/document/petite-vue-svg.html");
let allocator = Allocator::default();
let owner = NativeDocument::lex_in(&allocator, root(source));
assert!(owner.normal_completion().is_ok());
assert!(matches!(
owner.html_structure(),
Err(Refusal::UnsupportedElement(_))
));
}