Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ba4481a
LibWeb: Read selector identifiers through live DOM wrappers
awesomekling Jul 21, 2026
15be6d8
LibWeb: Match common tag selectors through live DOM wrappers
awesomekling Jul 21, 2026
38d943e
LibWeb: Read root selector state from the live DOM wrapper
awesomekling Jul 21, 2026
0c75a93
LibWeb: Compare selector element types through live wrappers
awesomekling Jul 21, 2026
8cb51e7
LibWeb: Read selector element facts through live accessors
awesomekling Jul 21, 2026
e99c1dc
LibWeb: Resolve selector namespaces through live accessors
awesomekling Jul 21, 2026
c575bb9
LibWeb: Match tag names through live DOM string views
awesomekling Jul 21, 2026
3a652c9
LibWeb: Match quirks classes through live DOM views
awesomekling Jul 21, 2026
710ea51
LibWeb: Match attributes through live DOM views
awesomekling Jul 21, 2026
82b59ee
LibWeb: Match link states through live DOM facts
awesomekling Jul 21, 2026
afbde3f
LibWeb: Match focus states through live DOM facts
awesomekling Jul 21, 2026
eaa6da1
LibWeb: Match fullscreen through a live DOM fact
awesomekling Jul 21, 2026
001df3b
LibWeb: Match headings through a live DOM fact
awesomekling Jul 21, 2026
89bd83b
LibWeb: Match open popovers through live DOM facts
awesomekling Jul 21, 2026
8ba821e
LibWeb: Match direction through a live DOM fact
awesomekling Jul 21, 2026
426605c
LibWeb: Match custom states through a live DOM query
awesomekling Jul 21, 2026
00adcea
LibWeb: Match language ranges in Rust
awesomekling Jul 21, 2026
f87c6ac
LibWeb: Match common states through live DOM facts
awesomekling Jul 21, 2026
31de365
LibWeb: Match media states through live DOM facts
awesomekling Jul 21, 2026
c50b632
LibWeb: Match remaining states through live DOM facts
awesomekling Jul 21, 2026
e50a76d
LibWeb: Treat selector navigation as live DOM reads
awesomekling Jul 21, 2026
5ad6954
LibWeb: Remove the retired selector callback counter
awesomekling Jul 21, 2026
a9b1d47
LibWeb: Remove redundant selector DOM wrapper state
awesomekling Jul 21, 2026
f71e80b
LibWeb: Remove the unused open-state parameter
awesomekling Jul 21, 2026
94681bc
LibWeb: Match foreign type selectors case-sensitively
awesomekling Jul 22, 2026
5f9a9c2
LibWeb: Match ID selectors case-insensitively in quirks mode
awesomekling Jul 22, 2026
c1a7fd1
LibWeb: Strengthen foreign type selector coverage
awesomekling Jul 22, 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
7 changes: 7 additions & 0 deletions Libraries/LibWeb/CSS/Rust/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,13 @@ fn main() -> Result<(), Box<dyn Error>> {
("FfiSimpleSelector", "SimpleSelector"),
("FfiCompoundSelector", "CompoundSelector"),
("FfiSelector", "Selector"),
("FfiElement", "Element"),
("FfiElementQualifiedName", "ElementQualifiedName"),
("FfiInternedStringList", "InternedStringList"),
("FfiDomStringView", "DomStringView"),
("FfiDomAttribute", "DomAttribute"),
("FfiResolvedNamespaceType", "ResolvedNamespaceType"),
("FfiResolvedNamespace", "ResolvedNamespace"),
("FfiElementAndShadowHost", "ElementAndShadowHost"),
] {
selector_config
Expand Down
3 changes: 1 addition & 2 deletions Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ define_ffi_ops! {
StyleGroupCloneEntry => "styleGroupCloneEntries",
StyleGroupFreeEntry => "styleGroupFreeEntries",
// Callbacks: Rust -> C++.
SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks",
SelectorTreeNavigationCallback => "selectorTreeNavigationCallbacks",
SelectorDomReadCallback => "selectorDomReadCallbacks",
SelectorMetadataCallback => "selectorMetadataCallbacks",
CascadePropertyDisallowedCallback => "cascadePropertyDisallowedCallbacks",
CascadeResolveUnresolvedCallback => "cascadeResolveUnresolvedCallbacks",
Expand Down
1,403 changes: 1,218 additions & 185 deletions Libraries/LibWeb/CSS/Rust/src/selector_engine.rs

Large diffs are not rendered by default.

1,159 changes: 512 additions & 647 deletions Libraries/LibWeb/CSS/SelectorMatching.cpp

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Libraries/LibWeb/CSS/SelectorRustBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,13 @@ class SelectorCompiler {
output.namespace_ = store_string(qualified_name.namespace_);
output.name = store_string(qualified_name.name.name);
output.lowercase_name = store_string(qualified_name.name.lowercase_name);
output.interned_name = reinterpret_cast<uintptr_t const*>(&qualified_name.name.name);
output.interned_lowercase_name = reinterpret_cast<uintptr_t const*>(&qualified_name.name.lowercase_name);
}

SelectorFFI::SimpleSelector compile_simple_selector(Selector::SimpleSelector const& simple_selector)
{
SelectorFFI::SimpleSelector output {};
// NB: The C++ simple selector outlives the compiled Rust selector, so matching callbacks
// can use this pointer to compare interned strings without copying them.
output.cxx_simple_selector = &simple_selector;

switch (simple_selector.type) {
case Selector::SimpleSelector::Type::Universal:
Expand All @@ -173,10 +172,12 @@ class SelectorCompiler {
case Selector::SimpleSelector::Type::Id:
output.selector_type = SelectorFFI::SimpleSelectorType::Id;
output.name = store_string(simple_selector.id_name());
output.interned_name = reinterpret_cast<uintptr_t const*>(&simple_selector.id_name());
break;
case Selector::SimpleSelector::Type::Class:
output.selector_type = SelectorFFI::SimpleSelectorType::Class;
output.name = store_string(simple_selector.class_name());
output.interned_name = reinterpret_cast<uintptr_t const*>(&simple_selector.class_name());
break;
case Selector::SimpleSelector::Type::Attribute: {
output.selector_type = SelectorFFI::SimpleSelectorType::Attribute;
Expand Down Expand Up @@ -230,6 +231,7 @@ class SelectorCompiler {

if (pseudo_class.ident.has_value()) {
output.identifier = store_string(pseudo_class.ident->string_value);
output.interned_name = reinterpret_cast<uintptr_t const*>(&pseudo_class.ident->string_value);
if (pseudo_class.ident->keyword == Keyword::Ltr)
output.direction = SelectorFFI::Direction::LeftToRight;
else if (pseudo_class.ident->keyword == Keyword::Rtl)
Expand Down
9 changes: 9 additions & 0 deletions Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5123,6 +5123,15 @@ Optional<Utf16String> Element::lang() const
return m_lang_value;
}

Optional<Utf16View> Element::lang_view() const
{
if (!m_lang_value.has_value())
(void)lang();
if (m_lang_value->is_empty())
return {};
return m_lang_value->utf16_view();
}

void Element::invalidate_lang_value()
{
if (m_lang_value.has_value()) {
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class WEB_API Element
void follow_the_hyperlink(Optional<Utf16String> hyperlink_suffix, HTML::UserNavigationInvolvement = HTML::UserNavigationInvolvement::None);

Optional<Utf16String> lang() const;
Optional<Utf16View> lang_view() const;
void invalidate_lang_value();

WebIDL::ExceptionOr<void> set_attribute_for_bindings(Utf16FlyString qualified_name, Variant<GC::Ref<TrustedTypes::TrustedHTML>, GC::Ref<TrustedTypes::TrustedScript>, GC::Ref<TrustedTypes::TrustedScriptURL>, Utf16String> const& value);
Expand Down
13 changes: 13 additions & 0 deletions Tests/LibWeb/Text/expected/css/live-media-state-selectors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
video:buffering: true
video:muted: false
video:paused: true
video:playing: false
video:seeking: false
video:stalled: false
video:muted after mutation: true
div:buffering: false
div:muted: false
div:paused: false
div:playing: false
div:seeking: false
div:stalled: false
10 changes: 10 additions & 0 deletions Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
initial match: true
initial color: rgb(1, 2, 3)
mutated match: true
stale match: false
mutated color: rgb(4, 5, 6)
disconnected match: true
shadow initial match: true
shadow mutated match: true
document root: true
disconnected html root: true
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ adjacent sibling: true
subsequent sibling: true
attribute insensitive: true
attribute sensitive: false
attribute after mutation: true/false
logical: true
positional: true
relative descendant: true
Expand All @@ -12,6 +13,7 @@ relative sibling: true
closest: true
query selector all: 3
XML tag case: true/false
XML slow tag case: true
XML namespace wildcard: true
XML attribute namespace wildcard: true
host: yes
Expand Down
19 changes: 19 additions & 0 deletions Tests/LibWeb/Text/input/css/live-media-state-selectors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<video></video>
<div></div>
<script>
test(() => {
const video = document.querySelector("video");
const div = document.querySelector("div");

for (const pseudoClass of ["buffering", "muted", "paused", "playing", "seeking", "stalled"])
println(`video:${pseudoClass}: ${video.matches(`:${pseudoClass}`)}`);

video.muted = true;
println(`video:muted after mutation: ${video.matches(":muted")}`);

for (const pseudoClass of ["buffering", "muted", "paused", "playing", "seeking", "stalled"])
println(`div:${pseudoClass}: ${div.matches(`:${pseudoClass}`)}`);
});
</script>
51 changes: 51 additions & 0 deletions Tests/LibWeb/Text/input/css/live-selector-identifiers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
#old-parent.old-parent-class #target.old-target-class {
color: rgb(1, 2, 3);
}

#new-parent.new-parent-class #target.new-target-class {
color: rgb(4, 5, 6);
}
</style>
<div id="old-parent" class="old-parent-class">
<span id="target" class="old-target-class"></span>
</div>
<div id="host"></div>
<script>
test(() => {
const target = document.querySelector("span");
const parent = target.parentElement;

println(`initial match: ${target.matches("#target.old-target-class")}`);
println(`initial color: ${getComputedStyle(target).color}`);

parent.id = "new-parent";
parent.className = "new-parent-class";
target.className = "new-target-class";

println(`mutated match: ${target.matches("#target.new-target-class")}`);
println(`stale match: ${target.matches("#target.old-target-class")}`);
println(`mutated color: ${getComputedStyle(target).color}`);

const disconnected = document.createElement("div");
disconnected.id = "a-long-disconnected-identifier";
disconnected.className = "short a-long-disconnected-class-name";
println(`disconnected match: ${disconnected.matches("#a-long-disconnected-identifier.a-long-disconnected-class-name")}`);

const shadow_root = document.querySelector("#host").attachShadow({ mode: "open" });
const shadow_target = document.createElement("span");
shadow_target.id = "shadow-old";
shadow_target.className = "shadow-old-class";
shadow_root.appendChild(shadow_target);
println(`shadow initial match: ${shadow_target.matches("#shadow-old.shadow-old-class")}`);
shadow_target.id = "shadow-new";
shadow_target.className = "shadow-new-class";
println(`shadow mutated match: ${shadow_target.matches("#shadow-new.shadow-new-class")}`);

const disconnected_html = document.createElement("html");
println(`document root: ${document.documentElement.matches(":root")}`);
println(`disconnected html root: ${disconnected_html.matches(":root")}`);
});
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
println(`subsequent sibling: ${document.querySelector(".third").matches(".first ~ .third")}`);
println(`attribute insensitive: ${outer.matches('[data-mode="mixed" i]')}`);
println(`attribute sensitive: ${outer.matches('[data-mode="mixed" s]')}`);
outer.setAttribute("data-mode", "changed");
println(`attribute after mutation: ${outer.matches('[data-mode="changed"]')}/${outer.matches('[data-mode="mixed" i]')}`);
println(`logical: ${second.matches(":is(.first, .second):not(.third)")}`);
println(`positional: ${second.matches(":nth-child(2 of .item)")}`);
println(`relative descendant: ${outer.matches(":has(.second .needle)")}`);
Expand All @@ -28,10 +30,12 @@
println(`query selector all: ${outer.querySelectorAll(":scope > .item").length}`);
const xml = document.implementation.createDocument("urn:example", "ex:root");
const child = xml.createElementNS("urn:example", "ex:Child");
child.setAttribute("other", "no");
child.setAttributeNS("urn:attributes", "attr:value", "yes");
xml.documentElement.appendChild(child);

println(`XML tag case: ${child.matches("Child")}/${child.matches("child")}`);
println(`XML slow tag case: ${child.matches("child:not(.missing)")}`);
println(`XML namespace wildcard: ${child.matches("*|Child")}`);
println(`XML attribute namespace wildcard: ${child.matches("[*|value]")}`);
const host = document.createElement("div");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!DOCTYPE quirks>
<script src="include.js"></script>
<div class="tEsT"></div>
<div class="other tEsT"></div>
<script>
test(() => {
let divElement = document.querySelector(".test");
Expand Down
Loading