LibWeb: Remove the C++ HTML tree builder

Delete the old C++ tree-construction implementation and helper classes
that became unused once the Rust parser is unconditional. Remove the C++
stack of open elements, active formatting elements, speculative mock
element, and tree-builder-only token storage.

Keep the C++ parser entry points that still own LibWeb DOM integration,
encoding detection, tokenizer bridging, incremental parsing, and the
speculative parser support used by resource discovery.
This commit is contained in:
Andreas Kling 2026-05-16 13:47:49 +02:00 committed by Andreas Kling
parent a7ece4b062
commit 29784ea397
16 changed files with 49 additions and 5324 deletions

View file

@ -644,10 +644,7 @@ set(SOURCES
HTML/Parser/HTMLToken.cpp
HTML/Parser/HTMLTokenizer.cpp
HTML/Parser/IncrementalDocumentParser.cpp
HTML/Parser/ListOfActiveFormattingElements.cpp
HTML/Parser/SpeculativeHTMLParser.cpp
HTML/Parser/SpeculativeMockElement.cpp
HTML/Parser/StackOfOpenElements.cpp
HTML/Path2D.cpp
HTML/Plugin.cpp
HTML/PluginArray.cpp

View file

@ -765,7 +765,6 @@ class HTMLParamElement;
class HTMLParser;
class HTMLParserEndState;
class SpeculativeHTMLParser;
struct SpeculativeMockElement;
class HTMLPictureElement;
class HTMLPreElement;
class HTMLProgressElement;

File diff suppressed because it is too large Load diff

View file

@ -11,12 +11,12 @@
#include <LibWeb/DOM/FragmentSerializationMode.h>
#include <LibWeb/Export.h>
#include <LibWeb/HTML/Parser/HTMLTokenizer.h>
#include <LibWeb/HTML/Parser/ListOfActiveFormattingElements.h>
#include <LibWeb/HTML/Parser/ParserScriptingMode.h>
#include <LibWeb/HTML/Parser/StackOfOpenElements.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Platform/Timer.h>
struct RustFfiHtmlParserHandle;
namespace Web::SVG {
class SVGScriptElement;
@ -26,36 +26,12 @@ class SVGScriptElement;
namespace Web::HTML {
class HTMLScriptElement;
#define ENUMERATE_INSERTION_MODES \
__ENUMERATE_INSERTION_MODE(Initial) \
__ENUMERATE_INSERTION_MODE(BeforeHTML) \
__ENUMERATE_INSERTION_MODE(BeforeHead) \
__ENUMERATE_INSERTION_MODE(InHead) \
__ENUMERATE_INSERTION_MODE(InHeadNoscript) \
__ENUMERATE_INSERTION_MODE(AfterHead) \
__ENUMERATE_INSERTION_MODE(InBody) \
__ENUMERATE_INSERTION_MODE(Text) \
__ENUMERATE_INSERTION_MODE(InTable) \
__ENUMERATE_INSERTION_MODE(InTableText) \
__ENUMERATE_INSERTION_MODE(InCaption) \
__ENUMERATE_INSERTION_MODE(InColumnGroup) \
__ENUMERATE_INSERTION_MODE(InTableBody) \
__ENUMERATE_INSERTION_MODE(InRow) \
__ENUMERATE_INSERTION_MODE(InCell) \
__ENUMERATE_INSERTION_MODE(InTemplate) \
__ENUMERATE_INSERTION_MODE(AfterBody) \
__ENUMERATE_INSERTION_MODE(InFrameset) \
__ENUMERATE_INSERTION_MODE(AfterFrameset) \
__ENUMERATE_INSERTION_MODE(AfterAfterBody) \
__ENUMERATE_INSERTION_MODE(AfterAfterFrameset)
class HTMLFormElement;
class WEB_API HTMLParser final : public JS::Cell {
GC_CELL(HTMLParser, JS::Cell);
GC_DECLARE_ALLOCATOR(HTMLParser);
friend class HTMLTokenizer;
public:
static constexpr bool OVERRIDES_FINALIZE = true;
@ -86,16 +62,6 @@ public:
};
static String serialize_html_fragment(DOM::Node const&, SerializableShadowRoots, Vector<GC::Root<DOM::ShadowRoot>> const&, DOM::FragmentSerializationMode = DOM::FragmentSerializationMode::Inner);
enum class InsertionMode {
#define __ENUMERATE_INSERTION_MODE(mode) mode,
ENUMERATE_INSERTION_MODES
#undef __ENUMERATE_INSERTION_MODE
};
InsertionMode insertion_mode() const { return m_insertion_mode; }
static bool is_special_tag(FlyString const& tag_name, Optional<FlyString> const& namespace_);
HTMLTokenizer& tokenizer() { return m_tokenizer; }
void configure_element_created_by_rust_parser(DOM::Element&);
@ -133,32 +99,6 @@ private:
virtual void initialize(JS::Realm&) override;
virtual void finalize() override;
char const* insertion_mode_name() const;
DOM::QuirksMode which_quirks_mode(HTMLToken const&) const;
void handle_initial(HTMLToken&);
void handle_before_html(HTMLToken&);
void handle_before_head(HTMLToken&);
void handle_in_head(HTMLToken&);
void handle_in_head_noscript(HTMLToken&);
void handle_after_head(HTMLToken&);
void handle_in_body(HTMLToken&);
void handle_after_body(HTMLToken&);
void handle_after_after_body(HTMLToken&);
void handle_text(HTMLToken&);
void handle_in_table(HTMLToken&);
void handle_in_table_body(HTMLToken&);
void handle_in_row(HTMLToken&);
void handle_in_cell(HTMLToken&);
void handle_in_table_text(HTMLToken&);
void handle_in_caption(HTMLToken&);
void handle_in_column_group(HTMLToken&);
void handle_in_template(HTMLToken&);
void handle_in_frameset(HTMLToken&);
void handle_after_frameset(HTMLToken&);
void handle_after_after_frameset(HTMLToken&);
void stop_parsing() { m_stop_parsing = true; }
// https://html.spec.whatwg.org/multipage/parsing.html#start-the-speculative-html-parser
@ -166,83 +106,22 @@ private:
// https://html.spec.whatwg.org/multipage/parsing.html#stop-the-speculative-html-parser
void stop_the_speculative_html_parser();
void generate_implied_end_tags(FlyString const& exception = {});
void generate_all_implied_end_tags_thoroughly();
GC::Ref<DOM::Element> create_element_for(HTMLToken const&, Optional<FlyString> const& namespace_, DOM::Node& intended_parent);
struct AdjustedInsertionLocation {
GC::Ptr<DOM::Node> parent;
GC::Ptr<DOM::Node> insert_before_sibling;
};
AdjustedInsertionLocation find_appropriate_place_for_inserting_node(GC::Ptr<DOM::Element> override_target = nullptr);
void insert_an_element_at_the_adjusted_insertion_location(GC::Ref<DOM::Element>);
DOM::Text* find_character_insertion_node();
void flush_character_insertions();
enum class OnlyAddToElementStack {
No,
Yes,
};
GC::Ref<DOM::Element> insert_foreign_element(HTMLToken const&, Optional<FlyString> const& namespace_, OnlyAddToElementStack);
GC::Ref<DOM::Element> insert_html_element(HTMLToken const&);
[[nodiscard]] GC::Ptr<DOM::Element> current_node();
[[nodiscard]] GC::Ptr<DOM::Element> adjusted_current_node();
[[nodiscard]] GC::Ptr<DOM::Element> node_before_current_node();
void insert_character(u32 data);
void insert_comment(HTMLToken&);
void reconstruct_the_active_formatting_elements();
void close_a_p_element();
void process_using_the_rules_for(InsertionMode, HTMLToken&);
void process_using_the_rules_for_foreign_content(HTMLToken&);
void parse_generic_raw_text_element(HTMLToken&);
void increment_script_nesting_level();
void decrement_script_nesting_level();
void reset_the_insertion_mode_appropriately();
void resume_after_parser_blocking_script();
void invoke_post_parse_action();
void handle_element_popped(DOM::Element&);
void adjust_mathml_attributes(HTMLToken&);
void adjust_svg_tag_names(HTMLToken&);
void adjust_svg_attributes(HTMLToken&);
static void adjust_foreign_attributes(HTMLToken&);
enum AdoptionAgencyAlgorithmOutcome {
DoNothing,
RunAnyOtherEndTagSteps,
};
AdoptionAgencyAlgorithmOutcome run_the_adoption_agency_algorithm(HTMLToken&);
void clear_the_stack_back_to_a_table_context();
void clear_the_stack_back_to_a_table_body_context();
void clear_the_stack_back_to_a_table_row_context();
void close_the_cell();
InsertionMode m_insertion_mode { InsertionMode::Initial };
InsertionMode m_original_insertion_mode { InsertionMode::Initial };
StackOfOpenElements m_stack_of_open_elements;
Vector<InsertionMode> m_stack_of_template_insertion_modes;
ListOfActiveFormattingElements m_list_of_active_formatting_elements;
HTMLTokenizer m_tokenizer;
RustFfiHtmlParserHandle* m_rust_parser { nullptr };
bool m_next_line_feed_can_be_ignored { false };
bool m_foster_parenting { false };
bool m_frameset_ok { true };
bool m_parsing_fragment { false };
// https://html.spec.whatwg.org/multipage/parsing.html#scripting-mode
ParserScriptingMode m_scripting_mode {};
bool m_script_created { false };
bool m_invoked_via_document_write { false };
bool m_aborted { false };
bool m_parser_pause_flag { false };
bool m_stop_parsing { false };
@ -254,17 +133,11 @@ private:
JS::Realm& realm();
GC::Ptr<DOM::Document> m_document;
GC::Ptr<HTMLHeadElement> m_head_element;
GC::Ptr<HTMLFormElement> m_form_element;
GC::Ptr<DOM::Element> m_context_element;
// https://html.spec.whatwg.org/multipage/parsing.html#active-speculative-html-parser
GC::Ptr<SpeculativeHTMLParser> m_active_speculative_html_parser;
Vector<HTMLToken> m_pending_table_character_tokens;
GC::Ptr<DOM::Text> m_character_insertion_node;
StringBuilder m_character_insertion_builder { StringBuilder::Mode::UTF16 };
};
class HTMLParserEndState final : public JS::Cell {

View file

@ -61,13 +61,6 @@ public:
bool force_quirks { false };
};
static HTMLToken make_character(u32 code_point)
{
HTMLToken token { Type::Character };
token.set_code_point(code_point);
return token;
}
static HTMLToken make_start_tag(FlyString const& tag_name)
{
HTMLToken token { Type::StartTag };
@ -109,23 +102,6 @@ public:
return m_data.get<u32>();
}
bool is_parser_whitespace() const
{
// NOTE: The parser considers '\r' to be whitespace, while the tokenizer does not.
if (!is_character())
return false;
switch (code_point()) {
case '\t':
case '\n':
case '\f':
case '\r':
case ' ':
return true;
default:
return false;
}
}
void set_code_point(u32 code_point)
{
VERIFY(is_character());
@ -168,25 +144,6 @@ public:
m_tag_self_closing = self_closing;
}
bool has_acknowledged_self_closing_flag() const
{
VERIFY(is_self_closing());
return m_tag_self_closing_acknowledged;
}
void acknowledge_self_closing_flag_if_set()
{
if (is_self_closing())
m_tag_self_closing_acknowledged = true;
}
bool has_attributes() const
{
VERIFY(is_start_tag() || is_end_tag());
auto* ptr = tag_attributes();
return ptr && !ptr->is_empty();
}
size_t attribute_count() const
{
VERIFY(is_start_tag() || is_end_tag());
@ -201,26 +158,6 @@ public:
ensure_tag_attributes().append(move(attribute));
}
Attribute const& last_attribute() const
{
VERIFY(is_start_tag() || is_end_tag());
VERIFY(has_attributes());
return tag_attributes()->last();
}
Attribute& last_attribute()
{
VERIFY(is_start_tag() || is_end_tag());
VERIFY(has_attributes());
return tag_attributes()->last();
}
void drop_attributes()
{
VERIFY(is_start_tag() || is_end_tag());
m_data.get<OwnPtr<Vector<Attribute>>>().clear();
}
void for_each_attribute(Function<IterationDecision(Attribute const&)> callback) const
{
VERIFY(is_start_tag() || is_end_tag());
@ -233,18 +170,6 @@ public:
}
}
void for_each_attribute(Function<IterationDecision(Attribute&)> callback)
{
VERIFY(is_start_tag() || is_end_tag());
auto* ptr = tag_attributes();
if (!ptr)
return;
for (auto& attribute : *ptr) {
if (callback(attribute) == IterationDecision::Break)
break;
}
}
Optional<String> attribute(FlyString const& attribute_name) const
{
if (auto result = raw_attribute(attribute_name); result.has_value())
@ -271,36 +196,6 @@ public:
return attribute(attribute_name).has_value();
}
void adjust_tag_name(FlyString const& old_name, FlyString const& new_name)
{
VERIFY(is_start_tag() || is_end_tag());
if (old_name == tag_name())
set_tag_name(new_name);
}
void adjust_attribute_name(FlyString const& old_name, FlyString const& new_name)
{
VERIFY(is_start_tag() || is_end_tag());
for_each_attribute([&](Attribute& attribute) {
if (old_name == attribute.local_name)
attribute.local_name = new_name;
return IterationDecision::Continue;
});
}
void adjust_foreign_attribute(FlyString const& old_name, Optional<FlyString> const& prefix, FlyString const& local_name, Optional<FlyString> const& namespace_)
{
VERIFY(is_start_tag() || is_end_tag());
for_each_attribute([&](Attribute& attribute) {
if (old_name == attribute.local_name) {
attribute.prefix = prefix;
attribute.local_name = local_name;
attribute.namespace_ = namespace_;
}
return IterationDecision::Continue;
});
}
DoctypeData const& doctype_data() const
{
VERIFY(is_doctype());
@ -356,7 +251,6 @@ private:
// Type::StartTag and Type::EndTag
bool m_tag_self_closing { false };
bool m_tag_self_closing_acknowledged { false };
// AD-HOC: We need to know if the token had duplicate attributes, as Content Security Policy disables the nonce
// attribute on the element that will be created from such a token.

View file

@ -9,12 +9,9 @@
#include <AK/FlyString.h>
#include <AK/Vector.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Parser/HTMLToken.h>
#include <LibWeb/HTML/Parser/HTMLTokenizer.h>
#include <LibWeb/HTMLTokenizerRustFFI.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
@ -106,9 +103,7 @@ Optional<HTMLToken> HTMLTokenizer::next_token(StopAtInsertionPoint stop_at_inser
{
RustFfiToken ffi;
bool stop = stop_at_insertion_point == StopAtInsertionPoint::Yes;
bool cdata_allowed = m_parser != nullptr
&& m_parser->adjusted_current_node()
&& m_parser->adjusted_current_node()->namespace_uri() != Namespace::HTML;
bool cdata_allowed = false;
if (!rust_html_tokenizer_next_token(m_tokenizer, &ffi, stop, cdata_allowed))
return {};
@ -242,21 +237,6 @@ void HTMLTokenizer::insert_eof()
rust_html_tokenizer_insert_eof(m_tokenizer);
}
bool HTMLTokenizer::is_eof_inserted()
{
return rust_html_tokenizer_is_eof_inserted(m_tokenizer);
}
void HTMLTokenizer::set_blocked(bool blocked)
{
rust_html_tokenizer_set_blocked(m_tokenizer, blocked);
}
bool HTMLTokenizer::is_blocked() const
{
return rust_html_tokenizer_is_blocked(m_tokenizer);
}
bool HTMLTokenizer::is_insertion_point_defined() const
{
return rust_html_tokenizer_is_insertion_point_defined(m_tokenizer);
@ -292,12 +272,6 @@ void HTMLTokenizer::abort()
rust_html_tokenizer_abort(m_tokenizer);
}
void HTMLTokenizer::switch_to(Badge<HTMLParser>, State new_state)
{
dbgln_if(TOKENIZER_TRACE_DEBUG, "[{}] Parser switches tokenizer state to {}", state_name(m_state), state_name(new_state));
switch_to(new_state);
}
void HTMLTokenizer::switch_to(State new_state)
{
dbgln_if(TOKENIZER_TRACE_DEBUG, "[{}] Switch to {}", state_name(m_state), state_name(new_state));
@ -305,9 +279,4 @@ void HTMLTokenizer::switch_to(State new_state)
rust_html_tokenizer_switch_state(m_tokenizer, static_cast<uint8_t>(new_state));
}
void HTMLTokenizer::visit_edges(GC::Cell::Visitor& visitor)
{
visitor.visit(m_parser);
}
}

View file

@ -10,17 +10,15 @@
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <LibGC/Cell.h>
#include <LibGC/Ptr.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/Parser/HTMLToken.h>
struct RustFfiTokenizerHandle;
struct RustFfiHtmlParserHandle;
namespace Web::HTML {
class HTMLParser;
#define ENUMERATE_TOKENIZER_STATES \
__ENUMERATE_TOKENIZER_STATE(Data) \
__ENUMERATE_TOKENIZER_STATE(RCDATA) \
@ -121,14 +119,8 @@ public:
};
Optional<HTMLToken> next_token(StopAtInsertionPoint = StopAtInsertionPoint::No);
void set_parser(Badge<HTMLParser>, HTMLParser& parser) { m_parser = &parser; }
void switch_to(Badge<HTMLParser>, State new_state);
void switch_to(State new_state);
void set_blocked(bool b);
bool is_blocked() const;
auto const& source() const { return m_source; }
String unparsed_input() const;
@ -138,7 +130,6 @@ public:
bool is_input_stream_closed() const { return m_input_stream_closed; }
void insert_input_at_insertion_point(StringView input);
void insert_eof();
bool is_eof_inserted();
bool is_insertion_point_defined() const;
bool is_insertion_point_reached();
@ -155,8 +146,6 @@ public:
void parser_did_run(Badge<HTMLParser>);
RustFfiTokenizerHandle* ffi_handle(Badge<HTMLParser>) { return m_tokenizer; }
void visit_edges(GC::Cell::Visitor&);
private:
static char const* state_name(State state)
{
@ -170,8 +159,6 @@ private:
VERIFY_NOT_REACHED();
}
GC::Ptr<HTMLParser> m_parser;
State m_state { State::Data };
String m_source;
bool m_input_stream_closed { false };

View file

@ -1,143 +0,0 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Lorenz Ackermann <me@lorenzackermann.xyz>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/Parser/ListOfActiveFormattingElements.h>
namespace Web::HTML {
static constexpr size_t NoahsArkCapacity = 3;
ListOfActiveFormattingElements::~ListOfActiveFormattingElements() = default;
void ListOfActiveFormattingElements::visit_edges(JS::Cell::Visitor& visitor)
{
for (auto& entry : m_entries)
visitor.visit(entry.element);
}
void ListOfActiveFormattingElements::ensure_noahs_ark_clause(DOM::Element& element)
{
Vector<Entry&> possible_matches;
for (size_t i = m_entries.size(); i > 0;) {
i--;
auto& entry = m_entries[i];
if (entry.is_marker())
break;
if (entry.element->local_name() == element.local_name()
&& entry.element->namespace_uri() == element.namespace_uri()
&& entry.token->attribute_count() == element.attribute_list_size())
possible_matches.append(entry);
}
if (possible_matches.size() < NoahsArkCapacity)
return;
// FIXME: the attributes should be compared as they where created by the parser
element.for_each_attribute([&](auto& name, auto& value) {
possible_matches.remove_all_matching([&](auto& entry) {
auto attr = entry.token->attribute(name);
return !attr.has_value() || attr != value;
});
});
if (possible_matches.size() < NoahsArkCapacity)
return;
remove(*possible_matches.last().element);
}
AK::OwnPtr<HTMLToken> ListOfActiveFormattingElements::create_own_token(HTMLToken& token)
{
auto new_token = make<HTMLToken>(token.type());
new_token->set_tag_name(token.tag_name());
token.for_each_attribute([&](auto const& attribute) {
new_token->add_attribute(attribute);
return IterationDecision::Continue;
});
return new_token;
}
// https://html.spec.whatwg.org/multipage/parsing.html#push-onto-the-list-of-active-formatting-elements
void ListOfActiveFormattingElements::add(DOM::Element& element, HTMLToken& token)
{
// 1. If there are already three elements in the list of active formatting elements after the last marker, if any, or anywhere in the list if there are no markers,
// that have the same tag name, namespace, and attributes as element, then remove the earliest such element from the list of active formatting elements.
// For these purposes, the attributes must be compared as they were when the elements were created by the parser; two elements have the same attributes if all their parsed attributes
// can be paired such that the two attributes in each pair have identical names, namespaces, and values (the order of the attributes does not matter).
ensure_noahs_ark_clause(element);
// 2. Add element to the list of active formatting elements.
m_entries.append({ element, create_own_token(token) });
}
void ListOfActiveFormattingElements::add_marker()
{
m_entries.append({ nullptr, nullptr });
}
bool ListOfActiveFormattingElements::contains(DOM::Element const& element) const
{
for (auto& entry : m_entries) {
if (entry.element.ptr() == &element)
return true;
}
return false;
}
DOM::Element* ListOfActiveFormattingElements::last_element_with_tag_name_before_marker(FlyString const& tag_name)
{
for (ssize_t i = m_entries.size() - 1; i >= 0; --i) {
auto& entry = m_entries[i];
if (entry.is_marker())
return nullptr;
if (entry.element->local_name() == tag_name)
return entry.element.ptr();
}
return nullptr;
}
void ListOfActiveFormattingElements::remove(DOM::Element& element)
{
m_entries.remove_first_matching([&](auto& entry) {
return entry.element.ptr() == &element;
});
}
void ListOfActiveFormattingElements::clear_up_to_the_last_marker()
{
while (!m_entries.is_empty()) {
auto entry = m_entries.take_last();
if (entry.is_marker())
break;
}
}
Optional<size_t> ListOfActiveFormattingElements::find_index(DOM::Element const& element) const
{
for (size_t i = 0; i < m_entries.size(); i++) {
if (m_entries[i].element.ptr() == &element)
return i;
}
return {};
}
void ListOfActiveFormattingElements::replace(DOM::Element& to_remove, DOM::Element& to_add, HTMLToken& token)
{
for (auto& entry : m_entries) {
if (entry.element.ptr() == &to_remove) {
entry.element = GC::make_root(to_add);
entry.token = create_own_token(token);
break;
}
}
}
void ListOfActiveFormattingElements::insert_at(size_t index, DOM::Element& element, HTMLToken& token)
{
m_entries.insert(index, { element, create_own_token(token) });
}
}

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/Parser/HTMLToken.h>
namespace Web::HTML {
class ListOfActiveFormattingElements {
public:
ListOfActiveFormattingElements() = default;
~ListOfActiveFormattingElements();
struct Entry {
bool is_marker() const { return !element; }
GC::Ptr<DOM::Element> element;
AK::OwnPtr<HTMLToken> token;
};
bool is_empty() const { return m_entries.is_empty(); }
bool contains(DOM::Element const&) const;
void add(DOM::Element& element, HTMLToken& token);
void add_marker();
void insert_at(size_t index, DOM::Element& element, HTMLToken& token);
void replace(DOM::Element& to_remove, DOM::Element& to_add, HTMLToken& token);
void remove(DOM::Element&);
Vector<Entry> const& entries() const { return m_entries; }
Vector<Entry>& entries() { return m_entries; }
DOM::Element* last_element_with_tag_name_before_marker(FlyString const& tag_name);
void clear_up_to_the_last_marker();
Optional<size_t> find_index(DOM::Element const&) const;
void visit_edges(JS::Cell::Visitor&);
static AK::OwnPtr<HTMLToken> create_own_token(HTMLToken& token);
private:
Vector<Entry> m_entries;
void ensure_noahs_ark_clause(DOM::Element& element);
};
}

View file

@ -558,28 +558,6 @@ pub unsafe extern "C" fn rust_html_tokenizer_is_insertion_point_reached(handle:
handle.tokenizer.is_insertion_point_reached()
}
/// # Safety
/// `handle` must be a valid pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_tokenizer_set_blocked(handle: *mut RustFfiTokenizerHandle, blocked: bool) {
if handle.is_null() {
return;
}
let handle = unsafe { &mut *handle };
handle.tokenizer.set_blocked(blocked);
}
/// # Safety
/// `handle` must be a valid pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_tokenizer_is_blocked(handle: *mut RustFfiTokenizerHandle) -> bool {
if handle.is_null() {
return false;
}
let handle = unsafe { &mut *handle };
handle.tokenizer.is_blocked()
}
/// # Safety
/// `handle` must be a valid pointer.
#[unsafe(no_mangle)]
@ -605,17 +583,6 @@ pub unsafe extern "C" fn rust_html_tokenizer_insert_eof(handle: *mut RustFfiToke
handle.tokenizer.insert_eof();
}
/// # Safety
/// `handle` must be a valid pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_html_tokenizer_is_eof_inserted(handle: *mut RustFfiTokenizerHandle) -> bool {
if handle.is_null() {
return false;
}
let handle = unsafe { &mut *handle };
handle.tokenizer.is_eof_inserted()
}
/// # Safety
/// `handle` must be a valid pointer.
#[unsafe(no_mangle)]

View file

@ -223,9 +223,7 @@ pub struct HtmlTokenizer {
aborted: bool,
insertion_point: Option<usize>,
old_insertion_points: Vec<Option<usize>>,
explicit_eof_inserted: bool,
input_stream_closed: bool,
blocked: bool,
stop_at_insertion_point: bool,
cdata_allowed: bool,
entity_matcher: NamedCharacterReferenceMatcher,
@ -352,9 +350,7 @@ impl HtmlTokenizer {
aborted: false,
insertion_point: None,
old_insertion_points: Vec::new(),
explicit_eof_inserted: false,
input_stream_closed: true,
blocked: false,
stop_at_insertion_point: false,
cdata_allowed: false,
entity_matcher: NamedCharacterReferenceMatcher::new(),
@ -439,23 +435,10 @@ impl HtmlTokenizer {
self.current_column = last_position.column;
}
pub fn set_blocked(&mut self, blocked: bool) {
self.blocked = blocked;
}
pub fn is_blocked(&self) -> bool {
self.blocked
}
pub fn insert_eof(&mut self) {
self.explicit_eof_inserted = true;
self.input_stream_closed = true;
}
pub fn is_eof_inserted(&self) -> bool {
self.explicit_eof_inserted
}
pub fn set_input_stream_closed(&mut self, closed: bool) {
self.input_stream_closed = closed;
}

View file

@ -14,7 +14,6 @@
#include <LibWeb/HTML/CORSSettingAttribute.h>
#include <LibWeb/HTML/Parser/HTMLToken.h>
#include <LibWeb/HTML/Parser/SpeculativeHTMLParser.h>
#include <LibWeb/HTML/Parser/SpeculativeMockElement.h>
#include <LibWeb/HTML/PotentialCORSRequest.h>
#include <LibWeb/HTML/PreloadEntry.h>
#include <LibWeb/HTML/Scripting/Environments.h>
@ -44,7 +43,6 @@ void SpeculativeHTMLParser::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_document);
m_tokenizer.visit_edges(visitor);
}
void SpeculativeHTMLParser::stop()
@ -74,6 +72,26 @@ void SpeculativeHTMLParser::run()
namespace {
// https://html.spec.whatwg.org/multipage/parsing.html#speculative-mock-element
struct SpeculativeMockElement {
// local name
FlyString local_name;
// attribute list
Vector<HTMLToken::Attribute> attribute_list;
// FIXME: namespace and children: populate when speculative tree-building is implemented.
Optional<String> attribute(FlyString const& name) const
{
for (auto const& attribute : attribute_list) {
if (attribute.local_name == name)
return attribute.value;
}
return {};
}
};
Vector<HTMLToken::Attribute> attributes_from_token(HTMLToken const& token)
{
Vector<HTMLToken::Attribute> attributes;
@ -84,6 +102,24 @@ Vector<HTMLToken::Attribute> attributes_from_token(HTMLToken const& token)
return attributes;
}
// https://html.spec.whatwg.org/multipage/parsing.html#create-a-speculative-mock-element
SpeculativeMockElement create_a_speculative_mock_element(FlyString tag_name, Vector<HTMLToken::Attribute> attributes)
{
// 1. Let element be a new speculative mock element.
// 2. FIXME: Set element's namespace to namespace.
// 3. Set element's local name to tagName.
// 4. Set element's attribute list to attributes.
// 5. FIXME: Set element's children to a new empty list.
// 6. Optionally, perform a speculative fetch for element.
// The speculative fetch is performed by the caller (see SpeculativeHTMLParser::process_start_tag).
// 7. Return element.
return SpeculativeMockElement {
.local_name = move(tag_name),
.attribute_list = move(attributes),
};
}
void issue_speculative_fetch(JS::Realm& realm, DOM::Document& document, URL::URL url, Optional<Fetch::Infrastructure::Request::Destination> destination, CORSSettingAttribute cors_setting)
{
auto& vm = realm.vm();

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/Parser/SpeculativeMockElement.h>
namespace Web::HTML {
Optional<String> SpeculativeMockElement::attribute(FlyString const& name) const
{
for (auto const& attribute : attribute_list) {
if (attribute.local_name == name)
return attribute.value;
}
return {};
}
// https://html.spec.whatwg.org/multipage/parsing.html#create-a-speculative-mock-element
SpeculativeMockElement create_a_speculative_mock_element(FlyString tag_name, Vector<HTMLToken::Attribute> attributes)
{
// 1. Let element be a new speculative mock element.
// 2. FIXME: Set element's namespace to namespace.
// 3. Set element's local name to tagName.
// 4. Set element's attribute list to attributes.
// 5. FIXME: Set element's children to a new empty list.
// 6. Optionally, perform a speculative fetch for element.
// The speculative fetch is performed by the caller (see SpeculativeHTMLParser::process_start_tag).
// 7. Return element.
return SpeculativeMockElement {
.local_name = move(tag_name),
.attribute_list = move(attributes),
};
}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <AK/Optional.h>
#include <AK/Vector.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/Parser/HTMLToken.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/parsing.html#speculative-mock-element
struct SpeculativeMockElement {
// local name
FlyString local_name;
// attribute list
Vector<HTMLToken::Attribute> attribute_list;
// FIXME: namespace and children — populate when speculative tree-building is implemented.
Optional<String> attribute(FlyString const& name) const;
};
// https://html.spec.whatwg.org/multipage/parsing.html#create-a-speculative-mock-element
SpeculativeMockElement create_a_speculative_mock_element(FlyString tag_name, Vector<HTMLToken::Attribute> attributes);
}

View file

@ -1,202 +0,0 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Parser/StackOfOpenElements.h>
#include <LibWeb/MathML/TagNames.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/TagNames.h>
namespace Web::HTML {
static Vector<FlyString> s_base_list {
"applet"_fly_string,
"caption"_fly_string,
"html"_fly_string,
"table"_fly_string,
"td"_fly_string,
"th"_fly_string,
"marquee"_fly_string,
"object"_fly_string,
"select"_fly_string,
"template"_fly_string
};
StackOfOpenElements::~StackOfOpenElements() = default;
void StackOfOpenElements::visit_edges(JS::Cell::Visitor& visitor)
{
visitor.visit(m_elements);
}
GC::Ref<DOM::Element> StackOfOpenElements::pop()
{
auto element = m_elements.take_last();
if (m_on_element_popped)
m_on_element_popped(*element);
return *element;
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope
bool StackOfOpenElements::has_in_scope_impl(FlyString const& tag_name, Vector<FlyString> const& list, CheckMathAndSVG check_math_and_svg) const
{
for (auto const& element : m_elements.in_reverse()) {
if (element->namespace_uri() == Namespace::HTML && element->local_name() == tag_name)
return true;
if (element->namespace_uri() == Namespace::HTML && list.contains_slow(element->local_name()))
return false;
if (check_math_and_svg == CheckMathAndSVG::Yes && element->namespace_uri() == Namespace::SVG && element->local_name().is_one_of(SVG::TagNames::foreignObject, SVG::TagNames::desc, SVG::TagNames::title))
return false;
if (check_math_and_svg == CheckMathAndSVG::Yes && element->namespace_uri() == Namespace::MathML && element->local_name().is_one_of(MathML::TagNames::mi, MathML::TagNames::mo, MathML::TagNames::mn, MathML::TagNames::ms, MathML::TagNames::mtext, MathML::TagNames::annotation_xml))
return false;
}
VERIFY_NOT_REACHED();
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-scope
bool StackOfOpenElements::has_in_scope(FlyString const& tag_name) const
{
return has_in_scope_impl(tag_name, s_base_list, CheckMathAndSVG::Yes);
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope
bool StackOfOpenElements::has_in_scope_impl(DOM::Element const& target_node, Vector<FlyString> const& list) const
{
for (auto& element : m_elements.in_reverse()) {
if (element.ptr() == &target_node)
return true;
if (element->namespace_uri() == Namespace::HTML && list.contains_slow(element->local_name()))
return false;
if (element->namespace_uri() == Namespace::SVG && element->local_name().is_one_of(SVG::TagNames::foreignObject, SVG::TagNames::desc, SVG::TagNames::title))
return false;
if (element->namespace_uri() == Namespace::MathML && element->local_name().is_one_of(MathML::TagNames::mi, MathML::TagNames::mo, MathML::TagNames::mn, MathML::TagNames::ms, MathML::TagNames::mtext, MathML::TagNames::annotation_xml))
return false;
}
VERIFY_NOT_REACHED();
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-scope
bool StackOfOpenElements::has_in_scope(DOM::Element const& target_node) const
{
return has_in_scope_impl(target_node, s_base_list);
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-button-scope
bool StackOfOpenElements::has_in_button_scope(FlyString const& tag_name) const
{
auto list = s_base_list;
list.append("button"_fly_string);
return has_in_scope_impl(tag_name, list, CheckMathAndSVG::Yes);
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-table-scope
bool StackOfOpenElements::has_in_table_scope(FlyString const& tag_name) const
{
return has_in_scope_impl(tag_name, { "html"_fly_string, "table"_fly_string, "template"_fly_string }, CheckMathAndSVG::No);
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-list-item-scope
bool StackOfOpenElements::has_in_list_item_scope(FlyString const& tag_name) const
{
auto list = s_base_list;
list.append("ol"_fly_string);
list.append("ul"_fly_string);
return has_in_scope_impl(tag_name, list, CheckMathAndSVG::Yes);
}
bool StackOfOpenElements::contains(DOM::Element const& element) const
{
for (auto& element_on_stack : m_elements) {
if (&element == element_on_stack.ptr())
return true;
}
return false;
}
bool StackOfOpenElements::contains_template_element() const
{
for (auto const& element : m_elements) {
if (element->namespace_uri() != Namespace::HTML)
continue;
if (element->local_name() == HTML::TagNames::template_)
return true;
}
return false;
}
void StackOfOpenElements::pop_until_an_element_with_tag_name_has_been_popped(FlyString const& tag_name)
{
while (m_elements.last()->namespace_uri() != Namespace::HTML || m_elements.last()->local_name() != tag_name)
(void)pop();
(void)pop();
}
GC::Ptr<DOM::Element> StackOfOpenElements::topmost_special_node_below(DOM::Element const& formatting_element)
{
GC::Ptr<DOM::Element> found_element = nullptr;
for (auto& element : m_elements.in_reverse()) {
if (element.ptr() == &formatting_element)
break;
if (HTMLParser::is_special_tag(element->local_name(), element->namespace_uri()))
found_element = element.ptr();
}
return found_element.ptr();
}
StackOfOpenElements::LastElementResult StackOfOpenElements::last_element_with_tag_name(FlyString const& tag_name)
{
for (ssize_t i = m_elements.size() - 1; i >= 0; --i) {
auto& element = m_elements[i];
if (element->local_name() == tag_name)
return { element.ptr(), i };
}
return { nullptr, -1 };
}
GC::Ptr<DOM::Element> StackOfOpenElements::element_immediately_above(DOM::Element const& target)
{
bool found_target = false;
for (auto& element : m_elements.in_reverse()) {
if (element.ptr() == &target) {
found_target = true;
} else if (found_target)
return element.ptr();
}
return nullptr;
}
void StackOfOpenElements::remove(DOM::Element const& element)
{
m_elements.remove_first_matching([&element](auto& other) {
return other.ptr() == &element;
});
}
void StackOfOpenElements::replace(DOM::Element const& to_remove, GC::Ref<DOM::Element> to_add)
{
for (size_t i = 0; i < m_elements.size(); i++) {
if (m_elements[i].ptr() == &to_remove) {
m_elements.remove(i);
m_elements.insert(i, to_add);
break;
}
}
}
void StackOfOpenElements::insert_immediately_below(GC::Ref<DOM::Element> element_to_add, DOM::Element const& target)
{
for (size_t i = 0; i < m_elements.size(); i++) {
if (m_elements[i].ptr() == &target) {
m_elements.insert(i + 1, element_to_add);
break;
}
}
}
}

View file

@ -1,79 +0,0 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Forward.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/parsing.html#stack-of-open-elements
class StackOfOpenElements {
public:
// Initially, the stack of open elements is empty.
// The stack grows downwards; the topmost node on the stack is the first one added to the stack,
// and the bottommost node of the stack is the most recently added node in the stack
// (notwithstanding when the stack is manipulated in a random access fashion as part of the handling for misnested tags).
StackOfOpenElements() = default;
~StackOfOpenElements();
DOM::Element& first() { return *m_elements.first(); }
DOM::Element& last() { return *m_elements.last(); }
bool is_empty() const { return m_elements.is_empty(); }
void push(GC::Ref<DOM::Element> element) { m_elements.append(element); }
GC::Ref<DOM::Element> pop();
void set_on_element_popped(Function<void(DOM::Element&)> on_element_popped) { m_on_element_popped = move(on_element_popped); }
void remove(DOM::Element const& element);
void replace(DOM::Element const& to_remove, GC::Ref<DOM::Element> to_add);
void insert_immediately_below(GC::Ref<DOM::Element> element_to_add, DOM::Element const& target);
DOM::Element const& current_node() const { return *m_elements.last(); }
DOM::Element& current_node() { return *m_elements.last(); }
bool has_in_scope(FlyString const& tag_name) const;
bool has_in_button_scope(FlyString const& tag_name) const;
bool has_in_table_scope(FlyString const& tag_name) const;
bool has_in_list_item_scope(FlyString const& tag_name) const;
bool has_in_scope(DOM::Element const&) const;
bool contains(DOM::Element const&) const;
[[nodiscard]] bool contains_template_element() const;
auto const& elements() const { return m_elements; }
auto& elements() { return m_elements; }
void pop_until_an_element_with_tag_name_has_been_popped(FlyString const& tag_name);
GC::Ptr<DOM::Element> topmost_special_node_below(DOM::Element const&);
struct LastElementResult {
GC::Ptr<DOM::Element> element;
ssize_t index;
};
LastElementResult last_element_with_tag_name(FlyString const&);
GC::Ptr<DOM::Element> element_immediately_above(DOM::Element const&);
void visit_edges(JS::Cell::Visitor&);
private:
enum class CheckMathAndSVG : u8 {
No,
Yes,
};
bool has_in_scope_impl(FlyString const& tag_name, Vector<FlyString> const&, CheckMathAndSVG) const;
bool has_in_scope_impl(DOM::Element const& target_node, Vector<FlyString> const&) const;
Vector<GC::Ref<DOM::Element>> m_elements;
Function<void(DOM::Element&)> m_on_element_popped;
};
}