Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
600f73d
LibWeb: Characterize selector matching behavior
awesomekling Jul 16, 2026
7372784
LibWeb: Add a Rust selector matching representation
awesomekling Jul 16, 2026
1da727c
LibWeb: Compile selectors into a Rust representation
awesomekling Jul 16, 2026
1fe4e02
LibWeb: Port selector traversal to Rust
awesomekling Jul 16, 2026
d444741
LibWeb: Match borrowed DOM nodes through Rust
awesomekling Jul 16, 2026
da5b7bd
LibWeb: Preserve :has() matching fast paths in Rust
awesomekling Jul 16, 2026
f4ebfe1
LibWeb: Use the Rust selector matcher
awesomekling Jul 16, 2026
5d79c0c
LibWeb: Remove the C++ selector engine
awesomekling Jul 16, 2026
0364739
LibWeb: Widen Rust An+B matching arithmetic
awesomekling Jul 16, 2026
2d83d5a
LibWeb: Count nth-child siblings under fragment roots
awesomekling Jul 16, 2026
25a1d16
LibWeb: Characterize ::slotted() argument matching against slot elements
awesomekling Jul 16, 2026
ceab72b
LibWeb: Reject slots in ::slotted() before matching the argument
awesomekling Jul 16, 2026
bbf9a35
LibWeb: Cover An+B matching with step size 1 and a negative offset
awesomekling Jul 16, 2026
c3fa9e5
LibWeb: Compare interned strings in Rust selector matching callbacks
awesomekling Jul 16, 2026
736374b
LibWeb: Harden the Rust selector matching FFI
awesomekling Jul 16, 2026
d7c53aa
LibWeb: Generate the Rust selector matching FFI
awesomekling Jul 16, 2026
21c5fb1
LibWeb: Document Rust selector matching
awesomekling Jul 17, 2026
e275bea
LibWeb: Tighten Rust selector FFI ownership
awesomekling Jul 17, 2026
a8dd9e0
LibWeb: Restore Rust :has() matching state with a guard
awesomekling Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ set(SOURCES
CSS/Screen.cpp
CSS/ScreenOrientation.cpp
CSS/Selector.cpp
CSS/SelectorEngine.cpp
CSS/SelectorRustBridge.cpp
CSS/SelectorMatching.cpp
CSS/Serialize.cpp
CSS/Size.cpp
CSS/Sizing.cpp
Expand Down Expand Up @@ -1304,7 +1305,11 @@ foreach(angle_target IN LISTS ANGLE_TARGETS)
target_compile_options(LibWeb PRIVATE $<TARGET_PROPERTY:${angle_target},INTERFACE_COMPILE_OPTIONS>)
endforeach()

import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libweb_rust FFI_HEADER RustFFI.h)
import_rust_crate(
MANIFEST_PATH Rust/Cargo.toml
CRATE_NAME libweb_rust
FFI_HEADERS RustFFI.h SelectorRustFFI.h HTML/Parser/RustFFI.h
)

set(content_blocker_rust_features "")
if (NOT BUILD_SHARED_LIBS)
Expand Down
142 changes: 10 additions & 132 deletions Libraries/LibWeb/CSS/Selector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
#include <LibWeb/CSS/AncestorFilter.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/Parser/ErrorReporter.h>
#include <LibWeb/CSS/SelectorRustBridge.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/SelectorRustFFI.h>

namespace Web::CSS {

Expand All @@ -29,60 +31,10 @@ static bool component_value_contains_nesting_selector(Parser::ComponentValue con
return false;
}

static bool can_selector_use_fast_matches(Selector const& selector)
{
for (auto const& compound_selector : selector.compound_selectors()) {
if (!first_is_one_of(compound_selector.combinator,
Selector::Combinator::None, Selector::Combinator::Descendant, Selector::Combinator::ImmediateChild)) {
return false;
}

for (auto const& simple_selector : compound_selector.simple_selectors) {
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoClass) {
auto const pseudo_class = simple_selector.pseudo_class().type;
if (!first_is_one_of(pseudo_class,
PseudoClass::Active,
PseudoClass::AnyLink,
PseudoClass::Autofill,
PseudoClass::Checked,
PseudoClass::Disabled,
PseudoClass::Empty,
PseudoClass::Enabled,
PseudoClass::FirstChild,
PseudoClass::Focus,
PseudoClass::FocusVisible,
PseudoClass::FocusWithin,
PseudoClass::Hover,
PseudoClass::LastChild,
PseudoClass::Link,
PseudoClass::LocalLink,
PseudoClass::OnlyChild,
PseudoClass::Root,
PseudoClass::State,
PseudoClass::Unchecked,
PseudoClass::Visited))
return false;
} else if (!first_is_one_of(simple_selector.type,
Selector::SimpleSelector::Type::TagName,
Selector::SimpleSelector::Type::Universal,
Selector::SimpleSelector::Type::Class,
Selector::SimpleSelector::Type::Id,
Selector::SimpleSelector::Type::Attribute)) {
return false;
}
}
}

return true;
}

Selector::Selector(Vector<CompoundSelector>&& compound_selectors)
: m_compound_selectors(move(compound_selectors))
{
for (auto const& compound_selector : m_compound_selectors) {
if (compound_selector.combinator == Combinator::PseudoElement)
m_contains_pseudo_element_transition = true;

for (auto const& simple_selector : compound_selector.simple_selectors) {
if (simple_selector.type != SimpleSelector::Type::PseudoElement)
continue;
Expand All @@ -94,16 +46,6 @@ Selector::Selector(Vector<CompoundSelector>&& compound_selectors)
}
}

if (!m_compound_selectors.is_empty()) {
auto const& rightmost_compound = m_compound_selectors.last();
if (!rightmost_compound.simple_selectors.is_empty()
&& rightmost_compound.simple_selectors.first().type == SimpleSelector::Type::PseudoElement) {
auto pseudo_element = rightmost_compound.simple_selectors.first().pseudo_element().type();
if (!first_is_one_of(pseudo_element, PseudoElement::Slotted, PseudoElement::Part))
m_target_pseudo_element = pseudo_element;
}
}

// https://drafts.csswg.org/css-nesting-1/#contain-the-nesting-selector
// "A selector is said to contain the nesting selector if, when it was parsed as any type of selector,
// a <delim-token> with the value "&" (U+0026 AMPERSAND) was encountered."
Expand Down Expand Up @@ -142,7 +84,14 @@ Selector::Selector(Vector<CompoundSelector>&& compound_selectors)

collect_ancestor_hashes();

m_can_use_fast_matches = can_selector_use_fast_matches(*this);
m_rust_selector = compile_selector_for_matching(*this);
VERIFY(m_rust_selector);
m_target_pseudo_element = pseudo_element_from_ffi(SelectorFFI::rust_selector_target_pseudo_element(m_rust_selector));
}

Selector::~Selector()
{
SelectorFFI::rust_selector_destroy(m_rust_selector);
}

static void append_integer(Utf16StringBuilder& builder, i64 value)
Expand Down Expand Up @@ -1098,36 +1047,6 @@ Optional<Selector::SimpleSelector> Selector::SimpleSelector::absolutized(Selecto
VERIFY_NOT_REACHED();
}

size_t Selector::sibling_invalidation_distance() const
{
if (m_sibling_invalidation_distance.has_value())
return *m_sibling_invalidation_distance;

m_sibling_invalidation_distance = 0;
size_t current_distance = 0;
for (auto const& compound_selector : compound_selectors()) {
if (compound_selector.combinator == Combinator::None || compound_selector.combinator == Combinator::PseudoElement)
continue;

if (compound_selector.combinator == Combinator::SubsequentSibling) {
m_sibling_invalidation_distance = NumericLimits<size_t>::max();
return *m_sibling_invalidation_distance;
}

if (compound_selector.combinator == Combinator::NextSibling) {
current_distance++;
} else {
m_sibling_invalidation_distance = max(*m_sibling_invalidation_distance, current_distance);
current_distance = 0;
}
}

if (current_distance > 0) {
m_sibling_invalidation_distance = max(*m_sibling_invalidation_distance, current_distance);
}
return *m_sibling_invalidation_distance;
}

SelectorList adapt_nested_relative_selector_list(SelectorList const& selectors, StyleNestingParent style_nesting_parent)
{
// "Nested style rules differ from non-nested rules in the following ways:
Expand Down Expand Up @@ -1243,47 +1162,6 @@ SelectorList absolutize_selectors_relative_to(SelectorList const& selectors, GC:
return absolutized_selectors;
}

// https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
bool Selector::SimpleSelector::ANPlusBPattern::matches(int index) const
{
// "If both a and b are equal to zero, the pseudo-class represents no element in the document tree."
if (step_size == 0 && offset == 0)
return false;

// When "step_size == -1", selector represents first "offset" elements in document tree.
if (step_size == -1)
return !(offset <= 0 || index > offset);

// When "step_size == 1", selector represents last "offset" elements in document tree.
if (step_size == 1)
return !(offset < 0 || index < offset);

// When "step_size == 0", selector picks only the "offset" element.
if (step_size == 0)
return index == offset;

// If both are negative, nothing can match.
if (step_size < 0 && offset < 0)
return false;

// Like "a % b", but handles negative integers correctly. Done in 64 bits because "index - offset" and "-step_size"
// overflow a 32-bit int for an extreme An+B value such as :nth-child(2n-2147483648).
auto const canonical_modulo = [](i64 a, i64 b) -> i64 {
i64 c = a % b;
if ((c < 0 && b > 0) || (c > 0 && b < 0)) {
c += b;
}
return c;
};

// When "step_size < 0", we start at "offset" and count backwards.
if (step_size < 0)
return index <= offset && canonical_modulo(static_cast<i64>(index) - offset, -static_cast<i64>(step_size)) == 0;

// Otherwise, we start at "offset" and count forwards.
return index >= offset && canonical_modulo(static_cast<i64>(index) - offset, step_size) == 0;
}

// https://drafts.csswg.org/css-syntax-3/#serializing-anb
Utf16String Selector::SimpleSelector::ANPlusBPattern::serialize() const
{
Expand Down
23 changes: 14 additions & 9 deletions Libraries/LibWeb/CSS/Selector.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@

namespace Web::CSS {

namespace SelectorFFI {

struct RustSelector;

}

using SelectorList = Vector<NonnullRefPtr<class Selector>>;

// This is a <complex-selector> in the spec. https://www.w3.org/TR/selectors-4/#complex
Expand Down Expand Up @@ -94,7 +100,6 @@ class Selector : public RefCounted<Selector> {
int step_size { 0 }; // "A"
int offset = { 0 }; // "B"

bool matches(int index) const;
Utf16String serialize() const;
void serialize_to(Utf16StringBuilder&) const;
};
Expand Down Expand Up @@ -243,11 +248,10 @@ class Selector : public RefCounted<Selector> {
return adopt_ref(*new Selector(move(compound_selectors)));
}

~Selector() = default;
~Selector();

Vector<CompoundSelector> const& compound_selectors() const { return m_compound_selectors; }
Optional<PseudoElement> target_pseudo_element() const { return m_target_pseudo_element; }
bool contains_pseudo_element_transition() const { return m_contains_pseudo_element_transition; }
NonnullRefPtr<Selector> relative_to(SimpleSelector const&) const;
bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; }
bool contains_pseudo_class(PseudoClass pseudo_class) const { return m_contained_pseudo_classes.get(pseudo_class); }
Expand All @@ -259,25 +263,25 @@ class Selector : public RefCounted<Selector> {

auto const& ancestor_hashes() const { return m_ancestor_hashes; }

bool can_use_fast_matches() const { return m_can_use_fast_matches; }
bool can_use_ancestor_filter() const { return m_can_use_ancestor_filter; }

size_t sibling_invalidation_distance() const;

bool is_slotted() const { return m_contains_slotted_pseudo_element; }
bool has_part_pseudo_element() const { return m_contains_part_pseudo_element; }

SelectorFFI::RustSelector const& rust_selector() const
{
VERIFY(m_rust_selector);
return *m_rust_selector;
}

private:
explicit Selector(Vector<CompoundSelector>&&);

Vector<CompoundSelector> m_compound_selectors;
mutable Optional<u32> m_specificity;
Optional<PseudoElement> m_target_pseudo_element;
mutable Optional<size_t> m_sibling_invalidation_distance;
bool m_can_use_fast_matches { false };
bool m_can_use_ancestor_filter { false };
bool m_contains_the_nesting_selector { false };
bool m_contains_pseudo_element_transition { false };
bool m_contains_slotted_pseudo_element { false };
bool m_contains_part_pseudo_element { false };

Expand All @@ -286,6 +290,7 @@ class Selector : public RefCounted<Selector> {
void collect_ancestor_hashes();

Array<u32, 8> m_ancestor_hashes;
SelectorFFI::RustSelector* m_rust_selector { nullptr };
};

bool is_legacy_single_colon_pseudo_element(PseudoElement);
Expand Down
Loading