mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 08:18:59 +00:00
more pseudoclass support
This commit is contained in:
82
src/browser/tests/element/pseudo_classes.html
Normal file
82
src/browser/tests/element/pseudo_classes.html
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
<p id="first">First</p>
|
||||||
|
<input type="checkbox" id="checkbox">
|
||||||
|
<span id="empty"></span>
|
||||||
|
<span id="nonempty">Content</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id="parsing">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Test that all pseudo-classes parse without throwing errors
|
||||||
|
const pseudoClasses = [
|
||||||
|
':modal', ':checked', ':disabled', ':enabled', ':indeterminate',
|
||||||
|
':valid', ':invalid', ':required', ':optional', ':in-range', ':out-of-range',
|
||||||
|
':placeholder-shown', ':read-only', ':read-write', ':default',
|
||||||
|
':hover', ':active', ':focus', ':focus-within', ':focus-visible',
|
||||||
|
':link', ':visited', ':any-link', ':target',
|
||||||
|
':root', ':empty', ':first-child', ':last-child', ':only-child',
|
||||||
|
':first-of-type', ':last-of-type', ':only-of-type',
|
||||||
|
':nth-child(2)', ':nth-last-child(2)', ':nth-of-type(2)', ':nth-last-of-type(2)',
|
||||||
|
':defined', ':not(div)', ':is(div, span)', ':where(p, a)', ':has(span)', ':lang(en)'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Each querySelector should not throw an error
|
||||||
|
let allPassed = true;
|
||||||
|
for (const pseudo of pseudoClasses) {
|
||||||
|
try {
|
||||||
|
container.querySelector(pseudo);
|
||||||
|
} catch (e) {
|
||||||
|
allPassed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testing.expectTrue(allPassed);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="empty">
|
||||||
|
{
|
||||||
|
const empty = document.querySelector(':empty');
|
||||||
|
testing.expectTrue(empty !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="root">
|
||||||
|
{
|
||||||
|
const html = document.querySelector(':root');
|
||||||
|
testing.expectTrue(html !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="has">
|
||||||
|
{
|
||||||
|
const result = document.querySelector(':has(span)');
|
||||||
|
testing.expectTrue(result !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="not">
|
||||||
|
{
|
||||||
|
const noDiv = document.querySelectorAll(':not(div)');
|
||||||
|
testing.expectTrue(noDiv.length > 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="is">
|
||||||
|
{
|
||||||
|
const isResult = document.querySelectorAll(':is(p, span, input)');
|
||||||
|
testing.expectTrue(isResult.length >= 4);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="where">
|
||||||
|
{
|
||||||
|
const whereResult = document.querySelectorAll(':where(p, span)');
|
||||||
|
testing.expectTrue(whereResult.length >= 3);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
62
src/browser/tests/element/selector_invalid.html
Normal file
62
src/browser/tests/element/selector_invalid.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="container">
|
||||||
|
<p>Test</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id="empty_functional">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Empty functional pseudo-classes should error
|
||||||
|
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':has()'));
|
||||||
|
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':not()'));
|
||||||
|
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':is()'));
|
||||||
|
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':where()'));
|
||||||
|
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':lang()'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="invalid_patterns">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Invalid nth patterns
|
||||||
|
testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(foo)'));
|
||||||
|
testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(-)'));
|
||||||
|
testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(+)'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="unknown_pseudo">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Unknown pseudo-classes
|
||||||
|
testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':unknown'));
|
||||||
|
testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':not-a-real-pseudo'));
|
||||||
|
testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':fake(test)'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="empty_selector">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Empty selectors
|
||||||
|
testing.expectError("Syntax Error", () => container.querySelector(''));
|
||||||
|
testing.expectError("Syntax Error", () => document.querySelectorAll(''));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="invalid_combinators">
|
||||||
|
{
|
||||||
|
const container = $('#container');
|
||||||
|
|
||||||
|
// Combinators with nothing after
|
||||||
|
testing.expectError("Error: InvalidSelector", () => container.querySelector('p >'));
|
||||||
|
testing.expectError("Error: InvalidSelector", () => container.querySelector('p +'));
|
||||||
|
testing.expectError("Error: InvalidSelector", () => container.querySelector('p ~'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -53,9 +53,8 @@ pub fn collect(
|
|||||||
_ = tw.next();
|
_ = tw.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundary = if (result.exclude_root) result.root else null;
|
|
||||||
while (tw.next()) |node| {
|
while (tw.next()) |node| {
|
||||||
if (matches(node, result.selector, boundary)) {
|
if (matches(node, result.selector, page)) {
|
||||||
try nodes.put(allocator, node, {});
|
try nodes.put(allocator, node, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,9 +70,8 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node {
|
|||||||
if (result.exclude_root) {
|
if (result.exclude_root) {
|
||||||
_ = tw.next();
|
_ = tw.next();
|
||||||
}
|
}
|
||||||
const boundary = if (result.exclude_root) result.root else null;
|
|
||||||
while (tw.next()) |node| {
|
while (tw.next()) |node| {
|
||||||
if (matches(node, optimized_selector, boundary)) {
|
if (matches(node, optimized_selector, page)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +173,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
|
|||||||
.segments = selector.segments[0 .. seg_idx + 1],
|
.segments = selector.segments[0 .. seg_idx + 1],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!matches(id_node, prefix_selector, null)) {
|
if (!matches(id_node, prefix_selector, page)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,23 +248,23 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool {
|
pub fn matches(node: *Node, selector: Selector.Selector, page: *Page) bool {
|
||||||
const el = node.is(Node.Element) orelse return false;
|
const el = node.is(Node.Element) orelse return false;
|
||||||
|
|
||||||
if (selector.segments.len == 0) {
|
if (selector.segments.len == 0) {
|
||||||
return matchesCompound(el, selector.first);
|
return matchesCompound(el, selector.first, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const last_segment = selector.segments[selector.segments.len - 1];
|
const last_segment = selector.segments[selector.segments.len - 1];
|
||||||
if (!matchesCompound(el, last_segment.compound)) {
|
if (!matchesCompound(el, last_segment.compound, page)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchSegments(node, selector, selector.segments.len - 1, root);
|
return matchSegments(node, selector, selector.segments.len - 1, null, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match segments backward, with support for backtracking on subsequent_sibling
|
// Match segments backward, with support for backtracking on subsequent_sibling
|
||||||
fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node) bool {
|
fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, page: *Page) bool {
|
||||||
const segment = selector.segments[segment_index];
|
const segment = selector.segments[segment_index];
|
||||||
const target_compound = if (segment_index == 0)
|
const target_compound = if (segment_index == 0)
|
||||||
selector.first
|
selector.first
|
||||||
@@ -274,9 +272,9 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
|||||||
selector.segments[segment_index - 1].compound;
|
selector.segments[segment_index - 1].compound;
|
||||||
|
|
||||||
const matched: ?*Node = switch (segment.combinator) {
|
const matched: ?*Node = switch (segment.combinator) {
|
||||||
.descendant => matchDescendant(node, target_compound, root),
|
.descendant => matchDescendant(node, target_compound, root, page),
|
||||||
.child => matchChild(node, target_compound, root),
|
.child => matchChild(node, target_compound, root, page),
|
||||||
.next_sibling => matchNextSibling(node, target_compound),
|
.next_sibling => matchNextSibling(node, target_compound, page),
|
||||||
.subsequent_sibling => {
|
.subsequent_sibling => {
|
||||||
// For subsequent_sibling, try all matching siblings with backtracking
|
// For subsequent_sibling, try all matching siblings with backtracking
|
||||||
var sibling = node.previousSibling();
|
var sibling = node.previousSibling();
|
||||||
@@ -286,13 +284,13 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (matchesCompound(sibling_el, target_compound)) {
|
if (matchesCompound(sibling_el, target_compound, page)) {
|
||||||
// If we're at the first segment, we found a match
|
// If we're at the first segment, we found a match
|
||||||
if (segment_index == 0) {
|
if (segment_index == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to match remaining segments from this sibling
|
// Try to match remaining segments from this sibling
|
||||||
if (matchSegments(s, selector, segment_index - 1, root)) {
|
if (matchSegments(s, selector, segment_index - 1, root, page)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// This sibling didn't work, try the next one
|
// This sibling didn't work, try the next one
|
||||||
@@ -309,7 +307,7 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
|||||||
if (segment_index == 0) {
|
if (segment_index == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return matchSegments(current, selector, segment_index - 1, root);
|
return matchSegments(current, selector, segment_index - 1, root, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsequent_sibling already handled its recursion above
|
// subsequent_sibling already handled its recursion above
|
||||||
@@ -317,12 +315,12 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find an ancestor that matches the compound (any distance up the tree)
|
// Find an ancestor that matches the compound (any distance up the tree)
|
||||||
fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node {
|
fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node {
|
||||||
var current = node._parent;
|
var current = node._parent;
|
||||||
|
|
||||||
while (current) |ancestor| {
|
while (current) |ancestor| {
|
||||||
if (ancestor.is(Node.Element)) |ancestor_el| {
|
if (ancestor.is(Node.Element)) |ancestor_el| {
|
||||||
if (matchesCompound(ancestor_el, compound)) {
|
if (matchesCompound(ancestor_el, compound, page)) {
|
||||||
return ancestor;
|
return ancestor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,7 +339,7 @@ fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Nod
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the direct parent if it matches the compound
|
// Find the direct parent if it matches the compound
|
||||||
fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node {
|
fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node {
|
||||||
const parent = node._parent orelse return null;
|
const parent = node._parent orelse return null;
|
||||||
|
|
||||||
// Don't match beyond the root boundary
|
// Don't match beyond the root boundary
|
||||||
@@ -354,7 +352,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node {
|
|||||||
|
|
||||||
const parent_el = parent.is(Node.Element) orelse return null;
|
const parent_el = parent.is(Node.Element) orelse return null;
|
||||||
|
|
||||||
if (matchesCompound(parent_el, compound)) {
|
if (matchesCompound(parent_el, compound, page)) {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +360,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the immediately preceding sibling if it matches the compound
|
// Find the immediately preceding sibling if it matches the compound
|
||||||
fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
|
||||||
var sibling = node.previousSibling();
|
var sibling = node.previousSibling();
|
||||||
|
|
||||||
// For next_sibling (+), we need the immediately preceding element sibling
|
// For next_sibling (+), we need the immediately preceding element sibling
|
||||||
@@ -374,7 +372,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Found an element - check if it matches
|
// Found an element - check if it matches
|
||||||
if (matchesCompound(sibling_el, compound)) {
|
if (matchesCompound(sibling_el, compound, page)) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
// we found an element, it wasn't a match, we're done
|
// we found an element, it wasn't a match, we're done
|
||||||
@@ -385,7 +383,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find any preceding sibling that matches the compound
|
// Find any preceding sibling that matches the compound
|
||||||
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
|
||||||
var sibling = node.previousSibling();
|
var sibling = node.previousSibling();
|
||||||
|
|
||||||
// For subsequent_sibling (~), check all preceding element siblings
|
// For subsequent_sibling (~), check all preceding element siblings
|
||||||
@@ -396,7 +394,7 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (matchesCompound(sibling_el, compound)) {
|
if (matchesCompound(sibling_el, compound, page)) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,17 +404,17 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matchesCompound(el: *Node.Element, compound: Selector.Compound) bool {
|
fn matchesCompound(el: *Node.Element, compound: Selector.Compound, page: *Page) bool {
|
||||||
// For compound selectors, ALL parts must match
|
// For compound selectors, ALL parts must match
|
||||||
for (compound.parts) |part| {
|
for (compound.parts) |part| {
|
||||||
if (!matchesPart(el, part)) {
|
if (!matchesPart(el, part, page)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matchesPart(el: *Node.Element, part: Part) bool {
|
fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool {
|
||||||
switch (part) {
|
switch (part) {
|
||||||
.id => |id| {
|
.id => |id| {
|
||||||
const element_id = el.getAttributeSafe("id") orelse return false;
|
const element_id = el.getAttributeSafe("id") orelse return false;
|
||||||
@@ -437,7 +435,7 @@ fn matchesPart(el: *Node.Element, part: Part) bool {
|
|||||||
return std.mem.eql(u8, element_tag, tag_name);
|
return std.mem.eql(u8, element_tag, tag_name);
|
||||||
},
|
},
|
||||||
.universal => return true,
|
.universal => return true,
|
||||||
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo),
|
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, page),
|
||||||
.attribute => |attr| return matchesAttribute(el, attr),
|
.attribute => |attr| return matchesAttribute(el, attr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,13 +495,79 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass) bool {
|
fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Page) bool {
|
||||||
|
const node = el.asNode();
|
||||||
switch (pseudo) {
|
switch (pseudo) {
|
||||||
|
// State pseudo-classes
|
||||||
.modal => return false,
|
.modal => return false,
|
||||||
.checked => {
|
.checked => {
|
||||||
const input = el.is(Node.Element.Html.Input) orelse return false;
|
const input = el.is(Node.Element.Html.Input) orelse return false;
|
||||||
return input.getChecked();
|
return input.getChecked();
|
||||||
},
|
},
|
||||||
|
.disabled => {
|
||||||
|
return el.getAttributeSafe("disabled") != null;
|
||||||
|
},
|
||||||
|
.enabled => {
|
||||||
|
return el.getAttributeSafe("disabled") == null;
|
||||||
|
},
|
||||||
|
.indeterminate => return false,
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
.valid => return false,
|
||||||
|
.invalid => return false,
|
||||||
|
.required => {
|
||||||
|
return el.getAttributeSafe("required") != null;
|
||||||
|
},
|
||||||
|
.optional => {
|
||||||
|
return el.getAttributeSafe("required") == null;
|
||||||
|
},
|
||||||
|
.in_range => return false,
|
||||||
|
.out_of_range => return false,
|
||||||
|
.placeholder_shown => return false,
|
||||||
|
.read_only => {
|
||||||
|
return el.getAttributeSafe("readonly") != null;
|
||||||
|
},
|
||||||
|
.read_write => {
|
||||||
|
return el.getAttributeSafe("readonly") == null;
|
||||||
|
},
|
||||||
|
.default => return false,
|
||||||
|
|
||||||
|
// User interaction
|
||||||
|
.hover => return false,
|
||||||
|
.active => return false,
|
||||||
|
.focus => {
|
||||||
|
const active = page.document._active_element orelse return false;
|
||||||
|
return active == el;
|
||||||
|
},
|
||||||
|
.focus_within => {
|
||||||
|
const active = page.document._active_element orelse return false;
|
||||||
|
return node.contains(active.asNode());
|
||||||
|
},
|
||||||
|
.focus_visible => return false,
|
||||||
|
|
||||||
|
// Link states
|
||||||
|
.link => return false,
|
||||||
|
.visited => return false,
|
||||||
|
.any_link => {
|
||||||
|
if (el.getTag() != .anchor) return false;
|
||||||
|
return el.getAttributeSafe("href") != null;
|
||||||
|
},
|
||||||
|
.target => {
|
||||||
|
const element_id = el.getAttributeSafe("id") orelse return false;
|
||||||
|
const location = page.document._location orelse return false;
|
||||||
|
const hash = location.getHash();
|
||||||
|
if (hash.len <= 1) return false;
|
||||||
|
return std.mem.eql(u8, element_id, hash[1..]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tree structural
|
||||||
|
.root => {
|
||||||
|
const parent = node.parentNode() orelse return false;
|
||||||
|
return parent._type == .document;
|
||||||
|
},
|
||||||
|
.empty => {
|
||||||
|
return node.firstChild() == null;
|
||||||
|
},
|
||||||
.first_child => return isFirstChild(el),
|
.first_child => return isFirstChild(el),
|
||||||
.last_child => return isLastChild(el),
|
.last_child => return isLastChild(el),
|
||||||
.only_child => return isFirstChild(el) and isLastChild(el),
|
.only_child => return isFirstChild(el) and isLastChild(el),
|
||||||
@@ -514,19 +578,87 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass) bool {
|
|||||||
.nth_last_child => |pattern| return matchesNthLastChild(el, pattern),
|
.nth_last_child => |pattern| return matchesNthLastChild(el, pattern),
|
||||||
.nth_of_type => |pattern| return matchesNthOfType(el, pattern),
|
.nth_of_type => |pattern| return matchesNthOfType(el, pattern),
|
||||||
.nth_last_of_type => |pattern| return matchesNthLastOfType(el, pattern),
|
.nth_last_of_type => |pattern| return matchesNthLastOfType(el, pattern),
|
||||||
|
|
||||||
|
// Custom elements
|
||||||
|
.defined => {
|
||||||
|
const tag_name = el.getTagNameLower();
|
||||||
|
if (std.mem.indexOfScalar(u8, tag_name, '-') == null) return true;
|
||||||
|
const registry = &page.window._custom_elements;
|
||||||
|
return registry.get(tag_name) != null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Functional
|
||||||
|
.lang => return false,
|
||||||
.not => |selectors| {
|
.not => |selectors| {
|
||||||
// CSS Level 4: :not() matches if NONE of the selectors match
|
|
||||||
// Each selector in the list is evaluated independently
|
|
||||||
for (selectors) |selector| {
|
for (selectors) |selector| {
|
||||||
if (matches(el.asNode(), selector, null)) {
|
if (matches(node, selector, page)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
.is => |selectors| {
|
||||||
|
for (selectors) |selector| {
|
||||||
|
if (matches(node, selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
.where => |selectors| {
|
||||||
|
for (selectors) |selector| {
|
||||||
|
if (matches(node, selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
.has => |selectors| {
|
||||||
|
for (selectors) |selector| {
|
||||||
|
var child = node.firstChild();
|
||||||
|
while (child) |c| {
|
||||||
|
const child_el = c.is(Node.Element) orelse {
|
||||||
|
child = c.nextSibling();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matches(child_el.asNode(), selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesHasDescendant(child_el, selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
child = c.nextSibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *Page) bool {
|
||||||
|
var child = el.asNode().firstChild();
|
||||||
|
while (child) |c| {
|
||||||
|
const child_el = c.is(Node.Element) orelse {
|
||||||
|
child = c.nextSibling();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matches(child_el.asNode(), selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesHasDescendant(child_el, selector, page)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
child = c.nextSibling();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn isFirstChild(el: *Node.Element) bool {
|
fn isFirstChild(el: *Node.Element) bool {
|
||||||
const node = el.asNode();
|
const node = el.asNode();
|
||||||
var sibling = node.previousSibling();
|
var sibling = node.previousSibling();
|
||||||
|
|||||||
@@ -395,21 +395,144 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
|||||||
return .{ .not = selectors.items };
|
return .{ .not = selectors.items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, name, "is")) {
|
||||||
|
var selectors: std.ArrayList(Selector.Selector) = .empty;
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
while (true) {
|
||||||
|
if (self.peek() == ')') break;
|
||||||
|
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||||
|
|
||||||
|
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||||
|
try selectors.append(arena, selector);
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
if (self.peek() == ',') {
|
||||||
|
self.input = self.input[1..];
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||||
|
self.input = self.input[1..];
|
||||||
|
|
||||||
|
if (selectors.items.len == 0) return error.InvalidPseudoClass;
|
||||||
|
return .{ .is = selectors.items };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, name, "where")) {
|
||||||
|
var selectors: std.ArrayList(Selector.Selector) = .empty;
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
while (true) {
|
||||||
|
if (self.peek() == ')') break;
|
||||||
|
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||||
|
|
||||||
|
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||||
|
try selectors.append(arena, selector);
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
if (self.peek() == ',') {
|
||||||
|
self.input = self.input[1..];
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||||
|
self.input = self.input[1..];
|
||||||
|
|
||||||
|
if (selectors.items.len == 0) return error.InvalidPseudoClass;
|
||||||
|
return .{ .where = selectors.items };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, name, "has")) {
|
||||||
|
var selectors: std.ArrayList(Selector.Selector) = .empty;
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
while (true) {
|
||||||
|
if (self.peek() == ')') break;
|
||||||
|
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||||
|
|
||||||
|
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||||
|
try selectors.append(arena, selector);
|
||||||
|
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
if (self.peek() == ',') {
|
||||||
|
self.input = self.input[1..];
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||||
|
self.input = self.input[1..];
|
||||||
|
|
||||||
|
if (selectors.items.len == 0) return error.InvalidPseudoClass;
|
||||||
|
return .{ .has = selectors.items };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, name, "lang")) {
|
||||||
|
_ = self.skipSpaces();
|
||||||
|
const lang_start = self.input;
|
||||||
|
var lang_i: usize = 0;
|
||||||
|
while (lang_i < lang_start.len and lang_start[lang_i] != ')') : (lang_i += 1) {}
|
||||||
|
if (lang_i == 0 or self.peek() == 0) return error.InvalidPseudoClass;
|
||||||
|
|
||||||
|
const lang = try arena.dupe(u8, std.mem.trim(u8, lang_start[0..lang_i], &std.ascii.whitespace));
|
||||||
|
self.input = lang_start[lang_i..];
|
||||||
|
|
||||||
|
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||||
|
self.input = self.input[1..];
|
||||||
|
|
||||||
|
return .{ .lang = lang };
|
||||||
|
}
|
||||||
|
|
||||||
return error.UnknownPseudoClass;
|
return error.UnknownPseudoClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (name.len) {
|
switch (name.len) {
|
||||||
|
4 => {
|
||||||
|
if (fastEql(name, "root")) return .root;
|
||||||
|
if (fastEql(name, "link")) return .link;
|
||||||
|
},
|
||||||
5 => {
|
5 => {
|
||||||
if (fastEql(name, "modal")) return .modal;
|
if (fastEql(name, "modal")) return .modal;
|
||||||
|
if (fastEql(name, "hover")) return .hover;
|
||||||
|
if (fastEql(name, "focus")) return .focus;
|
||||||
|
if (fastEql(name, "empty")) return .empty;
|
||||||
|
if (fastEql(name, "valid")) return .valid;
|
||||||
|
},
|
||||||
|
6 => {
|
||||||
|
if (fastEql(name, "active")) return .active;
|
||||||
|
if (fastEql(name, "target")) return .target;
|
||||||
},
|
},
|
||||||
7 => {
|
7 => {
|
||||||
if (fastEql(name, "checked")) return .checked;
|
if (fastEql(name, "checked")) return .checked;
|
||||||
|
if (fastEql(name, "visited")) return .visited;
|
||||||
|
if (fastEql(name, "enabled")) return .enabled;
|
||||||
|
if (fastEql(name, "invalid")) return .invalid;
|
||||||
|
if (fastEql(name, "default")) return .default;
|
||||||
|
if (fastEql(name, "defined")) return .defined;
|
||||||
|
},
|
||||||
|
8 => {
|
||||||
|
if (fastEql(name, "disabled")) return .disabled;
|
||||||
|
if (fastEql(name, "required")) return .required;
|
||||||
|
if (fastEql(name, "optional")) return .optional;
|
||||||
|
if (fastEql(name, "any-link")) return .any_link;
|
||||||
|
if (fastEql(name, "in-range")) return .in_range;
|
||||||
|
},
|
||||||
|
9 => {
|
||||||
|
if (fastEql(name, "read-only")) return .read_only;
|
||||||
},
|
},
|
||||||
10 => {
|
10 => {
|
||||||
if (fastEql(name, "only-child")) return .only_child;
|
if (fastEql(name, "only-child")) return .only_child;
|
||||||
if (fastEql(name, "last-child")) return .last_child;
|
if (fastEql(name, "last-child")) return .last_child;
|
||||||
|
if (fastEql(name, "read-write")) return .read_write;
|
||||||
},
|
},
|
||||||
11 => {
|
11 => {
|
||||||
if (fastEql(name, "first-child")) return .first_child;
|
if (fastEql(name, "first-child")) return .first_child;
|
||||||
@@ -417,9 +540,16 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
|||||||
12 => {
|
12 => {
|
||||||
if (fastEql(name, "only-of-type")) return .only_of_type;
|
if (fastEql(name, "only-of-type")) return .only_of_type;
|
||||||
if (fastEql(name, "last-of-type")) return .last_of_type;
|
if (fastEql(name, "last-of-type")) return .last_of_type;
|
||||||
|
if (fastEql(name, "focus-within")) return .focus_within;
|
||||||
|
if (fastEql(name, "out-of-range")) return .out_of_range;
|
||||||
},
|
},
|
||||||
13 => {
|
13 => {
|
||||||
if (fastEql(name, "first-of-type")) return .first_of_type;
|
if (fastEql(name, "first-of-type")) return .first_of_type;
|
||||||
|
if (fastEql(name, "focus-visible")) return .focus_visible;
|
||||||
|
if (fastEql(name, "indeterminate")) return .indeterminate;
|
||||||
|
},
|
||||||
|
17 => {
|
||||||
|
if (fastEql(name, "placeholder-shown")) return .placeholder_shown;
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
|
|||||||
const selectors = try Parser.parseList(arena, input, page);
|
const selectors = try Parser.parseList(arena, input, page);
|
||||||
|
|
||||||
for (selectors) |selector| {
|
for (selectors) |selector| {
|
||||||
if (List.matches(el.asNode(), selector, null)) {
|
if (List.matches(el.asNode(), selector, page)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +131,41 @@ pub const AttributeMatcher = union(enum) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const PseudoClass = union(enum) {
|
pub const PseudoClass = union(enum) {
|
||||||
|
// State pseudo-classes
|
||||||
modal,
|
modal,
|
||||||
checked,
|
checked,
|
||||||
|
disabled,
|
||||||
|
enabled,
|
||||||
|
indeterminate,
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
valid,
|
||||||
|
invalid,
|
||||||
|
required,
|
||||||
|
optional,
|
||||||
|
in_range,
|
||||||
|
out_of_range,
|
||||||
|
placeholder_shown,
|
||||||
|
read_only,
|
||||||
|
read_write,
|
||||||
|
default,
|
||||||
|
|
||||||
|
// User interaction
|
||||||
|
hover,
|
||||||
|
active,
|
||||||
|
focus,
|
||||||
|
focus_within,
|
||||||
|
focus_visible,
|
||||||
|
|
||||||
|
// Link states
|
||||||
|
link,
|
||||||
|
visited,
|
||||||
|
any_link,
|
||||||
|
target,
|
||||||
|
|
||||||
|
// Tree structural
|
||||||
|
root,
|
||||||
|
empty,
|
||||||
first_child,
|
first_child,
|
||||||
last_child,
|
last_child,
|
||||||
only_child,
|
only_child,
|
||||||
@@ -143,7 +176,16 @@ pub const PseudoClass = union(enum) {
|
|||||||
nth_last_child: NthPattern,
|
nth_last_child: NthPattern,
|
||||||
nth_of_type: NthPattern,
|
nth_of_type: NthPattern,
|
||||||
nth_last_of_type: NthPattern,
|
nth_last_of_type: NthPattern,
|
||||||
|
|
||||||
|
// Custom elements
|
||||||
|
defined,
|
||||||
|
|
||||||
|
// Functional
|
||||||
|
lang: []const u8,
|
||||||
not: []const Selector, // :not() - CSS Level 4: supports full selectors and comma-separated lists
|
not: []const Selector, // :not() - CSS Level 4: supports full selectors and comma-separated lists
|
||||||
|
is: []const Selector, // :is() - matches any of the selectors
|
||||||
|
where: []const Selector, // :where() - like :is() but with zero specificity
|
||||||
|
has: []const Selector, // :has() - element containing descendants matching selector
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NthPattern = struct {
|
pub const NthPattern = struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user