LibWeb: Complete Rust HTML tree construction

Finish the Rust implementation of the spec tree-construction algorithms
needed by the LibWeb test suite. Add the remaining table modes, foster
parenting, scope helpers, adoption agency handling, ruby/list/form and
select cases, frameset state, foreign-content edge cases, and parser
host callbacks.

Preserve behavior that depends on the C++ DOM integration, including
parser-created custom element reactions, fragment quirks mode, arbitrary
fragment namespaces, template fragment mode, fragment form ownership,
MathML annotation-xml boundaries, contextual fragment scripts, parser
script source positions, document.close() parser state, void-element
insertion, and duplicate attribute tracking.

Add focused tests for the parser edge cases that are easy to regress at
the boundary between the Rust tree builder and the C++ DOM host.
This commit is contained in:
Andreas Kling 2026-05-15 21:56:35 +02:00 committed by Andreas Kling
parent de12062515
commit 54879bc916
26 changed files with 3402 additions and 361 deletions

View file

@ -984,9 +984,17 @@ WebIDL::ExceptionOr<void> Document::close()
// 4. Insert an explicit "EOF" character at the end of the parser's input stream.
m_parser->tokenizer().insert_eof();
auto finish_script_created_parser = [this] {
m_parser->tokenizer().undefine_insertion_point();
m_parser->pop_all_open_elements();
// AD-HOC: This ensures that a load event is fired if the node navigable's container is an iframe.
completely_finish_loading();
};
// 5. If there is a pending parsing-blocking script, then return.
if (has_pending_parsing_blocking_script()) {
m_parser->set_post_parse_action([this] { completely_finish_loading(); });
m_parser->set_post_parse_action(move(finish_script_created_parser));
return {};
}
@ -995,12 +1003,11 @@ WebIDL::ExceptionOr<void> Document::close()
// run() may have paused on a blocking script (e.g. from document.write inside an inline script).
if (has_pending_parsing_blocking_script()) {
m_parser->set_post_parse_action([this] { completely_finish_loading(); });
m_parser->set_post_parse_action(move(finish_script_created_parser));
return {};
}
// AD-HOC: This ensures that a load event is fired if the node navigable's container is an iframe.
completely_finish_loading();
finish_script_created_parser();
return {};
}

View file

@ -201,6 +201,11 @@ void HTMLScriptElement::execute_script()
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
void HTMLScriptElement::prepare_script()
{
// AD-HOC: Don't prepare scripts while in the temporary document used for fragment parsing. They will be prepared
// when inserted into a real document, unless fragment parsing already marked them as already started.
if (document().is_temporary_document_for_fragment_parsing())
return;
// 1. If el's already started is true, then return.
if (m_already_started)
return;

View file

@ -9,6 +9,7 @@
#include <AK/Debug.h>
#include <AK/SourceLocation.h>
#include <AK/TemporaryChange.h>
#include <AK/Utf32View.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
@ -23,6 +24,7 @@
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/NamedNodeMap.h>
#include <LibWeb/DOM/ProcessingInstruction.h>
#include <LibWeb/DOM/QualifiedName.h>
#include <LibWeb/DOM/ShadowRoot.h>
@ -69,7 +71,36 @@ static inline void log_parse_error(SourceLocation const& location = SourceLocati
}
static DOM::Node& node_from_html_parser_ffi(size_t);
static HTMLParser& parser_from_html_parser_ffi(void*);
static RustFfiHtmlNamespace namespace_to_html_parser_ffi(Optional<FlyString> const&);
static RustFfiHtmlAttributeNamespace attribute_namespace_to_html_parser_ffi(Optional<FlyString> const&);
static RustFfiHtmlQuirksMode quirks_mode_to_html_parser_ffi(DOM::QuirksMode);
extern "C" void ladybird_html_parser_log_parse_error(void*, u8 const*, size_t);
extern "C" void ladybird_html_parser_stop_parsing(void*);
extern "C" bool ladybird_html_parser_parse_errors_enabled();
extern "C" void ladybird_html_parser_visit_node(void*, size_t);
extern "C" size_t ladybird_html_parser_document_node(void*);
extern "C" size_t ladybird_html_parser_document_html_element(void*);
extern "C" void ladybird_html_parser_set_document_quirks_mode(void*, RustFfiHtmlQuirksMode);
extern "C" size_t ladybird_html_parser_create_document_type(void*, u8 const*, size_t, u8 const*, size_t, u8 const*, size_t);
extern "C" size_t ladybird_html_parser_create_comment(void*, u8 const*, size_t);
extern "C" void ladybird_html_parser_insert_text(size_t, size_t, u8 const*, size_t);
extern "C" void ladybird_html_parser_add_missing_attribute(size_t, u8 const*, size_t, u8 const*, size_t);
extern "C" void ladybird_html_parser_remove_node(size_t);
extern "C" void ladybird_html_parser_handle_element_popped(size_t);
extern "C" void ladybird_html_parser_prepare_svg_script(void*, size_t, size_t);
extern "C" void ladybird_html_parser_set_script_source_line(void*, size_t, size_t);
extern "C" void ladybird_html_parser_mark_script_already_started(void*, size_t);
extern "C" size_t ladybird_html_parser_parent_node(size_t);
extern "C" size_t ladybird_html_parser_create_element(void*, size_t, RustFfiHtmlNamespace, u8 const*, size_t, u8 const*, size_t, RustFfiHtmlParserAttribute const*, size_t, bool, size_t, bool);
extern "C" void ladybird_html_parser_append_child(size_t, size_t);
extern "C" void ladybird_html_parser_insert_node(size_t, size_t, size_t, bool);
extern "C" void ladybird_html_parser_move_all_children(size_t, size_t);
extern "C" size_t ladybird_html_parser_template_content(size_t);
extern "C" size_t ladybird_html_parser_attach_declarative_shadow_root(size_t, RustFfiHtmlShadowRootMode, RustFfiHtmlSlotAssignmentMode, bool, bool, bool, bool);
extern "C" void ladybird_html_parser_set_template_content(size_t, size_t);
extern "C" bool ladybird_html_parser_allows_declarative_shadow_roots(size_t);
Optional<HTMLParserBackend> html_parser_backend_from_string(StringView backend)
{
@ -256,6 +287,8 @@ void HTMLParser::visit_edges(Cell::Visitor& visitor)
m_stack_of_open_elements.visit_edges(visitor);
m_list_of_active_formatting_elements.visit_edges(visitor);
m_tokenizer.visit_edges(visitor);
if (m_rust_parser)
rust_html_parser_visit_edges(m_rust_parser, &visitor);
}
void HTMLParser::initialize(JS::Realm& realm)
@ -276,14 +309,27 @@ void HTMLParser::run(HTMLTokenizer::StopAtInsertionPoint stop_at_insertion_point
m_rust_parser,
m_tokenizer.ffi_handle({}),
this,
m_scripting_mode != ParserScriptingMode::Disabled,
stop_at_insertion_point == HTMLTokenizer::StopAtInsertionPoint::Yes);
if (result == RustFfiHtmlParserRunResult::Ok)
break;
VERIFY(result == RustFfiHtmlParserRunResult::ExecuteScript);
auto script = rust_html_parser_take_pending_script(m_rust_parser);
VERIFY(script);
process_script_end_tag_from_rust_parser(as<HTMLScriptElement>(node_from_html_parser_ffi(script)));
if (result == RustFfiHtmlParserRunResult::ExecuteScript) {
auto script = rust_html_parser_take_pending_script(m_rust_parser);
VERIFY(script);
process_script_end_tag_from_rust_parser(as<HTMLScriptElement>(node_from_html_parser_ffi(script)));
continue;
}
if (result == RustFfiHtmlParserRunResult::ExecuteSvgScript) {
auto script = rust_html_parser_take_pending_svg_script(m_rust_parser);
VERIFY(script);
if (process_svg_script_end_tag_from_rust_parser(as<SVG::SVGScriptElement>(node_from_html_parser_ffi(script))))
break;
continue;
}
VERIFY_NOT_REACHED();
}
m_tokenizer.parser_did_run({});
@ -358,9 +404,22 @@ void HTMLParser::run(URL::URL const& url, HTMLTokenizer::StopAtInsertionPoint st
run_until_completion(stop_at_insertion_point);
}
void HTMLParser::pop_all_open_elements()
{
if (m_backend == HTMLParserBackend::Rust) {
rust_html_parser_pop_all_open_elements(m_rust_parser);
return;
}
while (!m_stack_of_open_elements.is_empty())
(void)m_stack_of_open_elements.pop();
}
void HTMLParser::configure_element_created_by_rust_parser(DOM::Element& element)
{
if (element.local_name() == HTML::TagNames::link && element.namespace_uri() == Namespace::HTML) {
// AD-HOC: Let <link> elements know which document they were originally parsed for.
// This is used for the render-blocking logic.
auto& link_element = as<HTMLLinkElement>(element);
link_element.set_parser_document({}, document());
link_element.set_was_enabled_when_created_by_parser({}, !element.has_attribute(HTML::AttributeNames::disabled));
@ -380,9 +439,11 @@ void HTMLParser::configure_element_created_by_rust_parser(DOM::Element& element)
GC::Ref<DOM::Element> HTMLParser::create_element_for_rust_parser(HTMLToken const& token, Optional<FlyString> const& namespace_, DOM::Node& intended_parent, bool had_duplicate_attribute, GC::Ptr<HTMLFormElement> form_element, bool has_template_element_on_stack)
{
TemporaryChange<GC::Ptr<HTMLFormElement>> suppress_cpp_form_element { m_form_element, {} };
auto element = create_element_for(token, namespace_, intended_parent);
configure_element_created_by_rust_parser(element);
// AD-HOC: See AD-HOC comment on Element.m_had_duplicate_attribute_during_tokenization about why this is done.
if (had_duplicate_attribute)
element->set_had_duplicate_attribute_during_tokenization({});
@ -464,6 +525,82 @@ bool HTMLParser::process_script_end_tag_from_rust_parser(HTMLScriptElement& scri
return m_parser_pause_flag;
}
void HTMLParser::prepare_svg_script_for_rust_parser(SVG::SVGScriptElement& script, size_t source_line_number)
{
// AD-HOC: For SVG script elements, set the parser-inserted flag before the element is inserted into the DOM.
// Otherwise inserted()/attribute_changed() would invoke process_the_script_element() with the flag still unset
// and bypass the parser-blocking fetch handling.
//
// https://html.spec.whatwg.org/multipage/parsing.html#scripting-mode
// The Fragment scripting mode treats parser-inserted scripts as if they were not parser-inserted, allowing, for
// example, executing scripts when applying a fragment created by createContextualFragment().
if (m_scripting_mode != ParserScriptingMode::Fragment)
script.set_parser_inserted({});
script.set_source_line_number({}, source_line_number);
}
void HTMLParser::set_script_source_line_from_rust_parser(DOM::Element& element, size_t source_line_number)
{
if (auto* html_script_element = as_if<HTML::HTMLScriptElement>(element)) {
html_script_element->set_source_line_number({}, source_line_number);
return;
}
if (auto* svg_script_element = as_if<SVG::SVGScriptElement>(element))
svg_script_element->set_source_line_number({}, source_line_number);
}
void HTMLParser::mark_script_already_started_from_rust_parser(HTMLScriptElement& script)
{
script.set_already_started(Badge<HTMLParser> {}, true);
}
void HTMLParser::stop_parsing_from_rust_parser()
{
stop_parsing();
}
bool HTMLParser::process_svg_script_end_tag_from_rust_parser(SVG::SVGScriptElement& script)
{
// Let the old insertion point have the same value as the current insertion point.
m_tokenizer.store_old_insertion_point();
// Let the insertion point be just before the next input character.
m_tokenizer.update_insertion_point();
// Increment the parser's script nesting level by one.
increment_script_nesting_level();
// Set the parser pause flag to true.
m_parser_pause_flag = true;
// Non-standard: Make sure the <script> element has up-to-date text content before processing the script.
flush_character_insertions();
// If the active speculative HTML parser is null and the user agent supports SVG, then Process the SVG script element according to the SVG rules. [SVG]
// The active speculative HTML parser is null here.
script.process_the_script_element();
// Decrement the parser's script nesting level by one.
decrement_script_nesting_level();
// If the parser's script nesting level is zero, then set the parser pause flag to false.
if (script_nesting_level() == 0)
m_parser_pause_flag = false;
// Let the insertion point have the value of the old insertion point.
m_tokenizer.restore_old_insertion_point();
// If the SVG script registered itself as a pending parsing-blocking script (external fetch in flight),
// pause the parser and schedule a resume check. The parser will resume from
// resume_after_parser_blocking_script when the fetch completes.
if (document().pending_parsing_blocking_svg_script()) {
m_parser_pause_flag = true;
schedule_resume_check();
}
return m_parser_pause_flag;
}
void HTMLParser::run_until_completion(HTMLTokenizer::StopAtInsertionPoint stop_at_insertion_point)
{
m_post_parse_action = [this] { the_end(*m_document, this); };
@ -520,10 +657,8 @@ void HTMLParser::the_end(GC::Ref<DOM::Document> document, GC::Ptr<HTMLParser> pa
document->update_readiness(HTML::DocumentReadyState::Interactive);
// 4. Pop all the nodes off the stack of open elements.
if (parser) {
while (!parser->m_stack_of_open_elements.is_empty())
(void)parser->m_stack_of_open_elements.pop();
}
if (parser)
parser->pop_all_open_elements();
// AD-HOC: Skip remaining steps when there's no browsing context.
// This happens when parsing HTML via DOMParser or similar mechanisms.
@ -5289,14 +5424,18 @@ WebIDL::ExceptionOr<Vector<GC::Root<DOM::Node>>> HTMLParser::parse_html_fragment
// 9. Set the parser's scripting mode to scriptingMode.
if (context_element.document().is_scripting_disabled())
scripting_mode = HTML::ParserScriptingMode::Disabled;
auto parser = HTMLParser::create(*temp_document, markup, scripting_mode, "utf-8"sv);
auto backend = default_html_parser_backend();
auto parser = HTMLParser::create(*temp_document, markup, scripting_mode, "utf-8"sv, backend);
parser->m_context_element = context_element;
parser->m_parsing_fragment = true;
// 10. Set the state of the HTML parser's tokenization stage as follows, switching on the context element:
bool const context_element_is_html = context_element.namespace_uri() == Namespace::HTML;
// - title
// - textarea
if (context_element.local_name().is_one_of(HTML::TagNames::title, HTML::TagNames::textarea)) {
if (context_element_is_html
&& context_element.local_name().is_one_of(HTML::TagNames::title, HTML::TagNames::textarea)) {
// Switch the tokenizer to the RCDATA state.
parser->m_tokenizer.switch_to({}, HTMLTokenizer::State::RCDATA);
}
@ -5305,23 +5444,24 @@ WebIDL::ExceptionOr<Vector<GC::Root<DOM::Node>>> HTMLParser::parse_html_fragment
// - iframe
// - noembed
// - noframes
else if (context_element.local_name().is_one_of(HTML::TagNames::style, HTML::TagNames::xmp, HTML::TagNames::iframe, HTML::TagNames::noembed, HTML::TagNames::noframes)) {
else if (context_element_is_html
&& context_element.local_name().is_one_of(HTML::TagNames::style, HTML::TagNames::xmp, HTML::TagNames::iframe, HTML::TagNames::noembed, HTML::TagNames::noframes)) {
// Switch the tokenizer to the RAWTEXT state.
parser->m_tokenizer.switch_to({}, HTMLTokenizer::State::RAWTEXT);
}
// - script
else if (context_element.local_name().is_one_of(HTML::TagNames::script)) {
else if (context_element_is_html && context_element.local_name().is_one_of(HTML::TagNames::script)) {
// Switch the tokenizer to the script data state.
parser->m_tokenizer.switch_to({}, HTMLTokenizer::State::ScriptData);
}
// - noscript
else if (context_element.local_name().is_one_of(HTML::TagNames::noscript)) {
else if (context_element_is_html && context_element.local_name().is_one_of(HTML::TagNames::noscript)) {
// If scripting mode is not Disabled, switch the tokenizer to the RAWTEXT state. Otherwise, leave the tokenizer in the data state.
if (scripting_mode != HTML::ParserScriptingMode::Disabled)
parser->m_tokenizer.switch_to({}, HTMLTokenizer::State::RAWTEXT);
}
// - plaintext
else if (context_element.local_name().is_one_of(HTML::TagNames::plaintext)) {
else if (context_element_is_html && context_element.local_name().is_one_of(HTML::TagNames::plaintext)) {
// Switch the tokenizer to the PLAINTEXT state.
parser->m_tokenizer.switch_to({}, HTMLTokenizer::State::PLAINTEXT);
}
@ -5354,16 +5494,48 @@ WebIDL::ExceptionOr<Vector<GC::Root<DOM::Node>>> HTMLParser::parse_html_fragment
// 17. Set the HTML parser's form element pointer to the nearest node to context that is a form element
// (going straight up the ancestor chain, and including the element itself, if it is a form element), if any.
// (If there is no such form element, the form element pointer keeps its initial value, null.)
parser->m_form_element = context_element.first_ancestor_of_type<HTMLFormElement>();
parser->m_form_element = as_if<HTMLFormElement>(context_element);
if (!parser->m_form_element)
parser->m_form_element = context_element.first_ancestor_of_type<HTMLFormElement>();
if (parser->m_backend == HTMLParserBackend::Rust) {
auto context_local_name = context_element.local_name().bytes_as_string_view();
auto context_namespace = context_element.namespace_uri();
auto context_namespace_ffi = namespace_to_html_parser_ffi(context_namespace);
StringView context_namespace_uri;
if (context_namespace_ffi == RustFfiHtmlNamespace::Other && context_namespace.has_value())
context_namespace_uri = context_namespace->bytes_as_string_view();
Vector<RustFfiHtmlParserAttribute> context_attributes;
if (auto attributes = context_element.attributes()) {
context_attributes.ensure_capacity(attributes->length());
for (size_t i = 0; i < attributes->length(); ++i) {
auto const* attribute = attributes->item(i);
auto local_name = attribute->local_name().bytes_as_string_view();
auto value = attribute->value().bytes_as_string_view();
auto prefix = attribute->prefix().map([](auto const& prefix) { return prefix.bytes_as_string_view(); });
context_attributes.unchecked_append({
reinterpret_cast<u8 const*>(local_name.characters_without_null_termination()),
local_name.length(),
prefix.has_value() ? reinterpret_cast<u8 const*>(prefix->characters_without_null_termination()) : nullptr,
prefix.has_value() ? prefix->length() : 0,
attribute_namespace_to_html_parser_ffi(attribute->namespace_uri()),
reinterpret_cast<u8 const*>(value.characters_without_null_termination()),
value.length(),
});
}
}
rust_html_parser_begin_fragment(
parser->m_rust_parser,
reinterpret_cast<size_t>(root.ptr()),
namespace_to_html_parser_ffi(context_element.namespace_uri()),
reinterpret_cast<size_t>(&context_element),
context_namespace_ffi,
reinterpret_cast<u8 const*>(context_namespace_uri.characters_without_null_termination()),
context_namespace_uri.length(),
reinterpret_cast<u8 const*>(context_local_name.characters_without_null_termination()),
context_local_name.length(),
context_attributes.data(),
context_attributes.size(),
quirks_mode_to_html_parser_ffi(temp_document->mode()),
parser->m_form_element ? reinterpret_cast<size_t>(parser->m_form_element.ptr()) : 0);
}
@ -6061,8 +6233,7 @@ void HTMLParser::abort()
m_document->update_readiness(DocumentReadyState::Interactive);
// 4. Pop all the nodes off the stack of open elements.
while (!m_stack_of_open_elements.is_empty())
m_stack_of_open_elements.pop();
pop_all_open_elements();
// 5. Update the current document readiness to "complete".
m_document->update_readiness(DocumentReadyState::Complete);
@ -6114,7 +6285,30 @@ static String string_from_html_parser_ffi(u8 const* ptr, size_t len)
return MUST(String::from_utf8(html_parser_ffi_string_view(ptr, len)));
}
static Optional<FlyString> namespace_from_html_parser_ffi(RustFfiHtmlNamespace namespace_)
extern "C" void ladybird_html_parser_log_parse_error(void* parser, u8 const* message_ptr, size_t message_len)
{
(void)parser_from_html_parser_ffi(parser);
dbgln_if(HTML_PARSER_DEBUG, "Rust parser parse error: {}", html_parser_ffi_string_view(message_ptr, message_len));
}
extern "C" void ladybird_html_parser_stop_parsing(void* parser)
{
parser_from_html_parser_ffi(parser).stop_parsing_from_rust_parser();
}
extern "C" bool ladybird_html_parser_parse_errors_enabled()
{
return HTML_PARSER_DEBUG;
}
extern "C" void ladybird_html_parser_visit_node(void* visitor, size_t node)
{
if (node == 0)
return;
static_cast<GC::Cell::Visitor*>(visitor)->visit(node_from_html_parser_ffi(node));
}
static Optional<FlyString> namespace_from_html_parser_ffi(RustFfiHtmlNamespace namespace_, u8 const* namespace_uri_ptr, size_t namespace_uri_len)
{
switch (namespace_) {
case RustFfiHtmlNamespace::Html:
@ -6123,6 +6317,10 @@ static Optional<FlyString> namespace_from_html_parser_ffi(RustFfiHtmlNamespace n
return Namespace::MathML;
case RustFfiHtmlNamespace::Svg:
return Namespace::SVG;
case RustFfiHtmlNamespace::Other:
if (namespace_uri_len == 0)
return {};
return fly_string_from_html_parser_ffi(namespace_uri_ptr, namespace_uri_len);
}
VERIFY_NOT_REACHED();
}
@ -6138,17 +6336,38 @@ static Optional<FlyString> attribute_namespace_from_html_parser_ffi(RustFfiHtmlA
return Namespace::XML;
case RustFfiHtmlAttributeNamespace::Xmlns:
return Namespace::XMLNS;
case RustFfiHtmlAttributeNamespace::Other:
// Only fragment context attributes use this sentinel; parser-created attributes do not cross this path with
// arbitrary namespace URIs.
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
static RustFfiHtmlAttributeNamespace attribute_namespace_to_html_parser_ffi(Optional<FlyString> const& namespace_)
{
if (namespace_ == Namespace::XLink)
return RustFfiHtmlAttributeNamespace::XLink;
if (namespace_ == Namespace::XML)
return RustFfiHtmlAttributeNamespace::Xml;
if (namespace_ == Namespace::XMLNS)
return RustFfiHtmlAttributeNamespace::Xmlns;
if (namespace_.has_value())
return RustFfiHtmlAttributeNamespace::Other;
return RustFfiHtmlAttributeNamespace::None;
}
static RustFfiHtmlNamespace namespace_to_html_parser_ffi(Optional<FlyString> const& namespace_)
{
if (!namespace_.has_value())
return RustFfiHtmlNamespace::Other;
if (namespace_ == Namespace::HTML)
return RustFfiHtmlNamespace::Html;
if (namespace_ == Namespace::MathML)
return RustFfiHtmlNamespace::MathMl;
if (namespace_ == Namespace::SVG)
return RustFfiHtmlNamespace::Svg;
return RustFfiHtmlNamespace::Html;
return RustFfiHtmlNamespace::Other;
}
static DOM::QuirksMode quirks_mode_from_html_parser_ffi(RustFfiHtmlQuirksMode mode)
@ -6164,6 +6383,19 @@ static DOM::QuirksMode quirks_mode_from_html_parser_ffi(RustFfiHtmlQuirksMode mo
VERIFY_NOT_REACHED();
}
static RustFfiHtmlQuirksMode quirks_mode_to_html_parser_ffi(DOM::QuirksMode mode)
{
switch (mode) {
case DOM::QuirksMode::No:
return RustFfiHtmlQuirksMode::No;
case DOM::QuirksMode::Limited:
return RustFfiHtmlQuirksMode::Limited;
case DOM::QuirksMode::Yes:
return RustFfiHtmlQuirksMode::Yes;
}
VERIFY_NOT_REACHED();
}
static HTMLParser& parser_from_html_parser_ffi(void* parser)
{
VERIFY(parser);
@ -6181,9 +6413,19 @@ extern "C" size_t ladybird_html_parser_document_node(void* parser)
return reinterpret_cast<size_t>(&parser_from_html_parser_ffi(parser).document());
}
extern "C" size_t ladybird_html_parser_document_html_element(void* parser)
{
auto* html_element = parser_from_html_parser_ffi(parser).document().document_element();
if (!html_element || !is<HTMLHtmlElement>(*html_element))
return 0;
return reinterpret_cast<size_t>(html_element);
}
extern "C" void ladybird_html_parser_set_document_quirks_mode(void* parser, RustFfiHtmlQuirksMode mode)
{
parser_from_html_parser_ffi(parser).document().set_quirks_mode(quirks_mode_from_html_parser_ffi(mode));
auto& document = parser_from_html_parser_ffi(parser).document();
if (!document.parser_cannot_change_the_mode())
document.set_quirks_mode(quirks_mode_from_html_parser_ffi(mode));
}
extern "C" size_t ladybird_html_parser_create_document_type(void* parser, u8 const* name_ptr, size_t name_len, u8 const* public_id_ptr, size_t public_id_len, u8 const* system_id_ptr, size_t system_id_len)
@ -6230,7 +6472,54 @@ extern "C" void ladybird_html_parser_insert_text(size_t parent, size_t before, u
MUST(parent_node.append_child(*text));
}
extern "C" size_t ladybird_html_parser_create_element(void* parser, size_t intended_parent, RustFfiHtmlNamespace namespace_, u8 const* local_name_ptr, size_t local_name_len, RustFfiHtmlParserAttribute const* attributes, size_t attribute_count, bool had_duplicate_attribute, size_t form_element, bool has_template_element_on_stack)
extern "C" void ladybird_html_parser_add_missing_attribute(size_t element, u8 const* local_name_ptr, size_t local_name_len, u8 const* value_ptr, size_t value_len)
{
auto& dom_element = as<DOM::Element>(node_from_html_parser_ffi(element));
auto local_name = fly_string_from_html_parser_ffi(local_name_ptr, local_name_len);
if (dom_element.has_attribute(local_name))
return;
dom_element.append_attribute(local_name, string_from_html_parser_ffi(value_ptr, value_len));
}
extern "C" void ladybird_html_parser_remove_node(size_t node)
{
node_from_html_parser_ffi(node).remove(true);
}
extern "C" void ladybird_html_parser_handle_element_popped(size_t element)
{
// https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element
// When an option element is popped off the stack of open elements of an HTML parser or XML parser,
// the user agent must run maybe clone an option into selectedcontent given the option element.
// AD-HOC: The Rust tree builder flushes buffered text before invoking this hook, so the option's content is
// up-to-date before cloning.
if (auto* option_element = as_if<HTML::HTMLOptionElement>(node_from_html_parser_ffi(element)))
MUST(option_element->maybe_clone_into_selectedcontent());
}
extern "C" void ladybird_html_parser_prepare_svg_script(void* parser, size_t element, size_t source_line_number)
{
parser_from_html_parser_ffi(parser).prepare_svg_script_for_rust_parser(as<SVG::SVGScriptElement>(node_from_html_parser_ffi(element)), source_line_number);
}
extern "C" void ladybird_html_parser_set_script_source_line(void* parser, size_t element, size_t source_line_number)
{
parser_from_html_parser_ffi(parser).set_script_source_line_from_rust_parser(as<DOM::Element>(node_from_html_parser_ffi(element)), source_line_number);
}
extern "C" void ladybird_html_parser_mark_script_already_started(void* parser, size_t element)
{
if (auto* script = as_if<HTMLScriptElement>(node_from_html_parser_ffi(element)))
parser_from_html_parser_ffi(parser).mark_script_already_started_from_rust_parser(*script);
}
extern "C" size_t ladybird_html_parser_parent_node(size_t node)
{
auto* parent = node_from_html_parser_ffi(node).parent();
return reinterpret_cast<size_t>(parent);
}
extern "C" size_t ladybird_html_parser_create_element(void* parser, size_t intended_parent, RustFfiHtmlNamespace namespace_, u8 const* namespace_uri_ptr, size_t namespace_uri_len, u8 const* local_name_ptr, size_t local_name_len, RustFfiHtmlParserAttribute const* attributes, size_t attribute_count, bool had_duplicate_attribute, size_t form_element, bool has_template_element_on_stack)
{
auto& html_parser = parser_from_html_parser_ffi(parser);
auto local_name = fly_string_from_html_parser_ffi(local_name_ptr, local_name_len);
@ -6253,7 +6542,7 @@ extern "C" size_t ladybird_html_parser_create_element(void* parser, size_t inten
GC::Ptr<HTMLFormElement> form_element_ptr;
if (form_element)
form_element_ptr = as<HTMLFormElement>(node_from_html_parser_ffi(form_element));
auto element = html_parser.create_element_for_rust_parser(token, namespace_from_html_parser_ffi(namespace_), intended_parent_node, had_duplicate_attribute, form_element_ptr, has_template_element_on_stack);
auto element = html_parser.create_element_for_rust_parser(token, namespace_from_html_parser_ffi(namespace_, namespace_uri_ptr, namespace_uri_len), intended_parent_node, had_duplicate_attribute, form_element_ptr, has_template_element_on_stack);
return reinterpret_cast<size_t>(element.ptr());
}
@ -6263,6 +6552,35 @@ extern "C" void ladybird_html_parser_append_child(size_t parent, size_t child)
MUST(node_from_html_parser_ffi(parent).append_child(node_from_html_parser_ffi(child)));
}
extern "C" void ladybird_html_parser_insert_node(size_t parent, size_t before, size_t child, bool queue_custom_element_reactions)
{
auto& parent_node = node_from_html_parser_ffi(parent);
auto& child_node = node_from_html_parser_ffi(child);
auto* child_element = as_if<DOM::Element>(child_node);
if (queue_custom_element_reactions && child_element)
relevant_similar_origin_window_agent(*child_element).custom_element_reactions_stack.element_queue_stack.append({});
if (!before) {
MUST(parent_node.append_child(child_node));
} else {
auto& before_node = node_from_html_parser_ffi(before);
parent_node.insert_before(child_node, &before_node, false);
}
if (queue_custom_element_reactions && child_element) {
auto queue = relevant_similar_origin_window_agent(*child_element).custom_element_reactions_stack.element_queue_stack.take_last();
Bindings::invoke_custom_element_reactions(queue);
}
}
extern "C" void ladybird_html_parser_move_all_children(size_t from, size_t to)
{
auto& from_node = node_from_html_parser_ffi(from);
auto& to_node = node_from_html_parser_ffi(to);
for (auto& child : from_node.children_as_vector())
MUST(to_node.append_child(from_node.remove_child(*child).release_value()));
}
extern "C" size_t ladybird_html_parser_template_content(size_t element)
{
auto& template_element = as<HTMLTemplateElement>(node_from_html_parser_ffi(element));

View file

@ -17,6 +17,12 @@
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Platform/Timer.h>
namespace Web::SVG {
class SVGScriptElement;
}
namespace Web::HTML {
class HTMLScriptElement;
@ -72,6 +78,7 @@ public:
void run(HTMLTokenizer::StopAtInsertionPoint = HTMLTokenizer::StopAtInsertionPoint::No);
void run(URL::URL const&, HTMLTokenizer::StopAtInsertionPoint = HTMLTokenizer::StopAtInsertionPoint::No);
void run_until_completion(HTMLTokenizer::StopAtInsertionPoint = HTMLTokenizer::StopAtInsertionPoint::No);
void pop_all_open_elements();
static void the_end(GC::Ref<DOM::Document>, GC::Ptr<HTMLParser> = nullptr);
@ -102,7 +109,12 @@ public:
void configure_element_created_by_rust_parser(DOM::Element&);
GC::Ref<DOM::Element> create_element_for_rust_parser(HTMLToken const&, Optional<FlyString> const& namespace_, DOM::Node& intended_parent, bool had_duplicate_attribute, GC::Ptr<HTMLFormElement>, bool has_template_element_on_stack);
void prepare_svg_script_for_rust_parser(SVG::SVGScriptElement&, size_t source_line_number);
void set_script_source_line_from_rust_parser(DOM::Element&, size_t source_line_number);
void mark_script_already_started_from_rust_parser(HTMLScriptElement&);
void stop_parsing_from_rust_parser();
bool process_script_end_tag_from_rust_parser(HTMLScriptElement&);
bool process_svg_script_end_tag_from_rust_parser(SVG::SVGScriptElement&);
// https://html.spec.whatwg.org/multipage/parsing.html#abort-a-parser
void abort();

View file

@ -327,6 +327,7 @@ public:
void set_start_position(Badge<HTMLTokenizer>, Position start_position) { m_start_position = start_position; }
void set_end_position(Badge<HTMLTokenizer>, Position end_position) { m_end_position = end_position; }
void set_had_duplicate_attribute(Badge<HTMLTokenizer>) { m_had_duplicate_attribute = true; }
void normalize_attributes();
bool had_duplicate_attribute() const { return m_had_duplicate_attribute; }

View file

@ -167,6 +167,8 @@ Optional<HTMLToken> HTMLTokenizer::next_token(StopAtInsertionPoint stop_at_inser
token.add_attribute(move(attribute));
}
token.normalize_attributes();
if (ffi.had_duplicate_attribute)
token.set_had_duplicate_attribute({});
break;
}
case HTMLToken::Type::Comment:

View file

@ -42,6 +42,7 @@ pub struct RustFfiToken {
pub token_type: u8,
pub code_point: u32,
pub self_closing: bool,
pub had_duplicate_attribute: bool,
/// If nonzero, an interned tag-name id (1-based index into
/// `interned_names::INTERNED_TAG_NAMES`). When set, `tag_name_ptr` /
@ -101,6 +102,7 @@ impl Default for RustFfiToken {
token_type: TokenType::Invalid as u8,
code_point: 0,
self_closing: false,
had_duplicate_attribute: false,
tag_name_id: 0,
tag_name_ptr: ptr::null(),
tag_name_len: 0,
@ -304,10 +306,11 @@ fn next_token_slow(
tag_name,
tag_name_id,
self_closing,
had_duplicate_attribute: _,
had_duplicate_attribute,
attributes,
} => {
out.self_closing = self_closing;
out.had_duplicate_attribute = had_duplicate_attribute;
// Tokenizer already resolved intern ids, so we trust tag_name_id.
out.tag_name_id = tag_name_id;
if tag_name_id == 0 {

File diff suppressed because it is too large Load diff

View file

@ -66,6 +66,7 @@ pub enum TokenPayload {
tag_name: String,
tag_name_id: u16,
self_closing: bool,
// AD-HOC: See AD-HOC comment on Element.m_had_duplicate_attribute_during_tokenization about why this is tracked.
had_duplicate_attribute: bool,
attributes: Vec<Attribute>,
},

View file

@ -0,0 +1,2 @@
after parser insertion: constructor,connected
at DOMContentLoaded: constructor,connected

View file

@ -0,0 +1,4 @@
connected
written parent: body
host child count: 3
body children: reentrant-writer,span

View file

@ -0,0 +1 @@
document.write after document.close reopens document: true

View file

@ -0,0 +1,2 @@
BackCompat
<p><table><tbody><tr><td>cell</td></tr></tbody></table></p>

View file

@ -0,0 +1,2 @@
HTML split start tag line: 17
SVG split start tag line: 19

View file

@ -0,0 +1,21 @@
option end tag closes option before p: true
optgroup end tag closes optgroup before p: true
MathML annotation-xml keeps breakout HTML inside form: true
annotation-xml innerHTML child namespace: true
namespaced annotation-xml encoding attribute stays foreign: true
namespace-only annotation-xml encoding attribute stays foreign: true
custom namespace innerHTML child namespace: true
template innerHTML starts in template mode for table rows: true
form innerHTML uses context as form pointer: true
annotation-xml contextual fragment child namespace: true
HTML script from contextual fragment executes when applied: true
SVG script from contextual fragment executes when applied: true
SVG title innerHTML tokenizes in data state: true
SVG title innerHTML treats title end tag as markup: true
SVG title contextual fragment creates HTML child: true
SVG desc innerHTML ignores frameset start tag: true
prefixed SVG desc innerHTML ignores frameset start tag: true
param start tag skips active formatting reconstruction: true
source start tag skips active formatting reconstruction: true
track start tag skips active formatting reconstruction: true
adoption agency fallback after marker closes stale span: true

View file

@ -0,0 +1,2 @@
svg write parent: body
body children: svg,span,script

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 66 tests
63 Pass
3 Fail
66 Pass
Pass html5lib_innerHTML_foreign-fragment.html 4917b7458e1fff6c5cb21d7baf6863cc9550c61e
Pass html5lib_innerHTML_foreign-fragment.html b6d19b8ccacd2fde354df002b856f649ae91b20e
Pass html5lib_innerHTML_foreign-fragment.html 0c2411aa96ee023941778adaa11977890b232dc3
@ -11,7 +10,7 @@ Pass html5lib_innerHTML_foreign-fragment.html af0d0fc82bcd7e5ba5bc40f781701552b1
Pass html5lib_innerHTML_foreign-fragment.html 0135b05656c198b96a9e0f94333aa2c0190ec795
Pass html5lib_innerHTML_foreign-fragment.html 60d4a82dede2a297d6306278a19897d021075c6e
Pass html5lib_innerHTML_foreign-fragment.html f862d10d81a600b69e7fabd1474ca854ce08cca7
Fail html5lib_innerHTML_foreign-fragment.html 5d1db05a97609488e6749ff191294713aec9a90f
Pass html5lib_innerHTML_foreign-fragment.html 5d1db05a97609488e6749ff191294713aec9a90f
Pass html5lib_innerHTML_foreign-fragment.html 8804aa42daebb5ff2ab0015c6e89d8e40e7a8610
Pass html5lib_innerHTML_foreign-fragment.html 34b599e68117799324663b39aa3ba469bffb2dcb
Pass html5lib_innerHTML_foreign-fragment.html cc2199d299947f304e204c867bed2c7e910d50cc
@ -48,8 +47,8 @@ Pass html5lib_innerHTML_foreign-fragment.html b4c43a4fcdaa1a4c753674c4f92987b70d
Pass html5lib_innerHTML_foreign-fragment.html 7021fb0933e92112b94ee54b923efb6bc71e3b07
Pass html5lib_innerHTML_foreign-fragment.html 7c4b1614e2180b6649f3b02cf5c4a9d735166e1b
Pass html5lib_innerHTML_foreign-fragment.html bdeacb9250263776c63c2c7f731717c389bdc34c
Fail html5lib_innerHTML_foreign-fragment.html 2c46c15bdba5835b0f2f0e9eb5bc8566047b7d6d
Fail html5lib_innerHTML_foreign-fragment.html 8dfcfbf823ad6c7b6f7b81efc352f29b4e41e8be
Pass html5lib_innerHTML_foreign-fragment.html 2c46c15bdba5835b0f2f0e9eb5bc8566047b7d6d
Pass html5lib_innerHTML_foreign-fragment.html 8dfcfbf823ad6c7b6f7b81efc352f29b4e41e8be
Pass html5lib_innerHTML_foreign-fragment.html 74a8a40517c6fe110f0c71af7efb56d459ea8227
Pass html5lib_innerHTML_foreign-fragment.html 0c1782eb0f62f616627f0132729d6a194f8b7546
Pass html5lib_innerHTML_foreign-fragment.html 9dc5a819afe33d6babc04edc8f92cb8045f0f006
@ -69,4 +68,4 @@ Pass html5lib_innerHTML_foreign-fragment.html ce74a1ba339d07982908cc088c9057957a
Pass html5lib_innerHTML_foreign-fragment.html b941cd3ca955b1025061b0ff0cda775f0edd16bc
Pass html5lib_innerHTML_foreign-fragment.html 69fb90a251264e4e80762fa9acecd2c0bffc0c4c
Pass html5lib_innerHTML_foreign-fragment.html f856588390b813aafc272f42800d31ba9a4844e6
Pass html5lib_innerHTML_foreign-fragment.html 4c871c875e73e61adb24de1d18fad01363982e21
Pass html5lib_innerHTML_foreign-fragment.html 4c871c875e73e61adb24de1d18fad01363982e21

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 41 tests
39 Pass
2 Fail
41 Pass
Pass html5lib_webkit02.html f50b8c15847159a6d2c6ecc2bd1e4a944ba5aae6
Pass html5lib_webkit02.html 326328ea805a2ebdde707e08567713f88a4cf8ab
Pass html5lib_webkit02.html 05138397908cfdad69a3bfe5da5a06098320b504
@ -43,5 +42,5 @@ Pass html5lib_webkit02.html a6c50b1f6bfbe3c55102d8cad0950d0b68cc6729
Pass html5lib_webkit02.html 411f313a1b92ac7be549c41ee6758f952dc2dced
Pass html5lib_webkit02.html 84467597648753feeb78793e2cc9196bc75857c2
Pass html5lib_webkit02.html ae6f2e0a014f620269920ceb12660ec708236846
Fail html5lib_webkit02.html fd7aea4db6702879b9f8f410b0400d9300ae9c05
Fail html5lib_webkit02.html bad5cceffaffe98e3a1522be5f7df3e3e179d500
Pass html5lib_webkit02.html fd7aea4db6702879b9f8f410b0400d9300ae9c05
Pass html5lib_webkit02.html bad5cceffaffe98e3a1522be5f7df3e3e179d500

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 41 tests
39 Pass
2 Fail
41 Pass
Pass html5lib_webkit02.html f50b8c15847159a6d2c6ecc2bd1e4a944ba5aae6
Pass html5lib_webkit02.html 326328ea805a2ebdde707e08567713f88a4cf8ab
Pass html5lib_webkit02.html 05138397908cfdad69a3bfe5da5a06098320b504
@ -43,5 +42,5 @@ Pass html5lib_webkit02.html a6c50b1f6bfbe3c55102d8cad0950d0b68cc6729
Pass html5lib_webkit02.html 411f313a1b92ac7be549c41ee6758f952dc2dced
Pass html5lib_webkit02.html 84467597648753feeb78793e2cc9196bc75857c2
Pass html5lib_webkit02.html ae6f2e0a014f620269920ceb12660ec708236846
Fail html5lib_webkit02.html fd7aea4db6702879b9f8f410b0400d9300ae9c05
Fail html5lib_webkit02.html bad5cceffaffe98e3a1522be5f7df3e3e179d500
Pass html5lib_webkit02.html fd7aea4db6702879b9f8f410b0400d9300ae9c05
Pass html5lib_webkit02.html bad5cceffaffe98e3a1522be5f7df3e3e179d500

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
let reactions = [];
customElements.define("parser-insertion-reaction", class extends HTMLElement {
constructor() {
super();
reactions.push("constructor");
}
connectedCallback() {
reactions.push("connected");
}
});
</script>
<parser-insertion-reaction></parser-insertion-reaction>
<script>
let reactionsAfterParserInsertion = reactions.join(",");
test(() => {
println(`after parser insertion: ${reactionsAfterParserInsertion}`);
println(`at DOMContentLoaded: ${reactions.join(",")}`);
});
</script>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
let events = [];
customElements.define("reentrant-writer", class extends HTMLElement {
connectedCallback() {
events.push("connected");
document.write("<span id='written'>written</span>");
}
});
document.write("<reentrant-writer id='host'></reentrant-writer>");
</script>
<script>
test(() => {
let host = document.getElementById("host");
let written = document.getElementById("written");
println(events.join(","));
println(`written parent: ${written.parentElement.localName}`);
println(`host child count: ${host.childNodes.length}`);
let bodyChildren = Array.from(document.body.children)
.filter(element => element.id !== "out")
.map(element => element.localName);
println(`body children: ${bodyChildren.join(",")}`);
});
</script>

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
let iframe = document.createElement("iframe");
document.body.appendChild(iframe);
let frameDocument = iframe.contentDocument;
frameDocument.open();
frameDocument.write("<p id=closed>closed</p>");
frameDocument.close();
frameDocument.write("<span id=after>after</span>");
let reopened = frameDocument.getElementById("closed") === null
&& frameDocument.getElementById("after") !== null;
println(`document.write after document.close reopens document: ${reopened}`);
});
</script>

View file

@ -0,0 +1,10 @@
<!DOCTYPE quirks>
<script src="../include.js"></script>
<div id="container"></div>
<script>
test(() => {
println(document.compatMode);
container.innerHTML = "<p><table><tr><td>cell</td></tr></table>";
println(container.innerHTML);
});
</script>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
removeTestErrorHandler();
let labels = ["HTML", "SVG"];
let lines = [];
window.onerror = (message, source, lineno) => {
lines.push(`${labels[lines.length]} split start tag line: ${lineno}`);
return true;
};
test(() => {
for (let line of lines)
println(line);
});
</script>
<script
>throw new Error("HTML split start tag");</script>
<svg><script
>throw new Error("SVG split start tag");</script></svg>

View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
const HTML_NS = "http://www.w3.org/1999/xhtml";
const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
const SVG_NS = "http://www.w3.org/2000/svg";
let container = document.createElement("div");
container.innerHTML = "<option id=option><annotation-xml><img></option><p id=after></p>";
let option = container.querySelector("#option");
let after = container.querySelector("#after");
println(`option end tag closes option before p: ${after.parentElement === container && !option.contains(after)}`);
container.innerHTML = "<optgroup id=optgroup><annotation-xml><img></optgroup><p id=after></p>";
let optgroup = container.querySelector("#optgroup");
after = container.querySelector("#after");
println(`optgroup end tag closes optgroup before p: ${after.parentElement === container && !optgroup.contains(after)}`);
container.innerHTML = "<form id=form><math><annotation-xml></form><b id=bold>bold</b>";
let form = container.querySelector("#form");
let bold = container.querySelector("#bold");
println(`MathML annotation-xml keeps breakout HTML inside form: ${bold.closest("form") === form}`);
let annotation = document.createElementNS(MATHML_NS, "annotation-xml");
annotation.setAttribute("encoding", "text/html");
annotation.innerHTML = "<span></span>";
println(`annotation-xml innerHTML child namespace: ${annotation.firstElementChild.namespaceURI === HTML_NS}`);
let namespacedAnnotation = document.createElementNS(MATHML_NS, "annotation-xml");
namespacedAnnotation.setAttributeNS("urn:ladybird:test", "x:encoding", "text/html");
namespacedAnnotation.innerHTML = "<foo></foo>";
println(`namespaced annotation-xml encoding attribute stays foreign: ${namespacedAnnotation.firstElementChild.namespaceURI === MATHML_NS}`);
let namespaceOnlyAnnotation = document.createElementNS(MATHML_NS, "annotation-xml");
namespaceOnlyAnnotation.setAttributeNS("urn:ladybird:test", "encoding", "text/html");
namespaceOnlyAnnotation.innerHTML = "<foo></foo>";
println(`namespace-only annotation-xml encoding attribute stays foreign: ${namespaceOnlyAnnotation.firstElementChild.namespaceURI === MATHML_NS}`);
const CUSTOM_NS = "urn:ladybird:test";
let custom = document.createElementNS(CUSTOM_NS, "x");
custom.innerHTML = "<y></y>";
println(`custom namespace innerHTML child namespace: ${custom.firstElementChild.namespaceURI === CUSTOM_NS}`);
let template = document.createElement("template");
template.innerHTML = "<tr><td id=template-cell>x</td></tr>";
let templateCell = template.content.querySelector("#template-cell");
println(`template innerHTML starts in template mode for table rows: ${templateCell?.parentElement?.localName === "tr" && templateCell.textContent === "x"}`);
let formFragmentContext = document.createElement("form");
formFragmentContext.innerHTML = "<form id=inner-form><input id=form-input></form><span id=form-after></span>";
let formInput = formFragmentContext.querySelector("#form-input");
let formAfter = formFragmentContext.querySelector("#form-after");
println(`form innerHTML uses context as form pointer: ${formFragmentContext.querySelector("#inner-form") === null && formInput?.parentElement === formFragmentContext && formAfter?.parentElement === formFragmentContext}`);
let range = document.createRange();
range.selectNodeContents(annotation);
let fragment = range.createContextualFragment("<span></span>");
println(`annotation-xml contextual fragment child namespace: ${fragment.firstElementChild.namespaceURI === HTML_NS}`);
window.htmlFragmentScriptRan = false;
range.selectNodeContents(document.body);
fragment = range.createContextualFragment("<script>window.htmlFragmentScriptRan = true;<\/script>");
document.body.appendChild(fragment);
println(`HTML script from contextual fragment executes when applied: ${window.htmlFragmentScriptRan}`);
document.body.lastElementChild.remove();
window.svgFragmentScriptRan = false;
range.selectNodeContents(document.body);
fragment = range.createContextualFragment("<svg><script>window.svgFragmentScriptRan = true;<\/script></svg>");
document.body.appendChild(fragment);
println(`SVG script from contextual fragment executes when applied: ${window.svgFragmentScriptRan}`);
document.body.lastElementChild.remove();
let svgTitle = document.createElementNS(SVG_NS, "title");
svgTitle.innerHTML = "<div id=svg-title-div></div>";
let svgTitleDiv = svgTitle.querySelector("#svg-title-div");
println(`SVG title innerHTML tokenizes in data state: ${svgTitleDiv?.namespaceURI === HTML_NS && svgTitle.childNodes.length === 1}`);
svgTitle.innerHTML = "</title>X";
println(`SVG title innerHTML treats title end tag as markup: ${svgTitle.childNodes.length === 1 && svgTitle.textContent === "X"}`);
range.selectNodeContents(svgTitle);
fragment = range.createContextualFragment("<figure></figure>");
println(`SVG title contextual fragment creates HTML child: ${fragment.firstElementChild?.namespaceURI === HTML_NS && fragment.firstElementChild?.localName === "figure"}`);
let svgDesc = document.createElementNS(SVG_NS, "desc");
svgDesc.innerHTML = "<frameset>X";
println(`SVG desc innerHTML ignores frameset start tag: ${svgDesc.childNodes.length === 1 && svgDesc.textContent === "X"}`);
let prefixedSvgDesc = document.createElementNS(SVG_NS, "svg:desc");
prefixedSvgDesc.innerHTML = "<frameset>X";
println(`prefixed SVG desc innerHTML ignores frameset start tag: ${prefixedSvgDesc.childNodes.length === 1 && prefixedSvgDesc.textContent === "X"}`);
for (const tagName of ["param", "source", "track"]) {
container.innerHTML = `<p><b>text</p><${tagName} id=void-element><span>after`;
let voidElement = container.querySelector("#void-element");
println(`${tagName} start tag skips active formatting reconstruction: ${voidElement.parentElement === container && !voidElement.closest("b")}`);
}
container.innerHTML = "<applet><b><b><b><b></b></b></b><span id=stale></b><p id=after></p>";
let stale = container.querySelector("#stale");
after = container.querySelector("#after");
println(`adoption agency fallback after marker closes stale span: ${!stale.contains(after) && !after.closest("b")}`);
});
</script>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<svg xmlns="http://www.w3.org/2000/svg">
<script>
document.write("<span id='from-svg'>svg</span>");
</script>
</svg>
<script>
test(() => {
let fromSVG = document.getElementById("from-svg");
println(`svg write parent: ${fromSVG.parentElement.localName}`);
let bodyChildren = Array.from(document.body.children)
.filter(element => element.id !== "out")
.map(element => element.localName);
println(`body children: ${bodyChildren.join(",")}`);
});
</script>