mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +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();
|
||||
}
|
||||
|
||||
const boundary = if (result.exclude_root) result.root else null;
|
||||
while (tw.next()) |node| {
|
||||
if (matches(node, result.selector, boundary)) {
|
||||
if (matches(node, result.selector, page)) {
|
||||
try nodes.put(allocator, node, {});
|
||||
}
|
||||
}
|
||||
@@ -71,9 +70,8 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node {
|
||||
if (result.exclude_root) {
|
||||
_ = tw.next();
|
||||
}
|
||||
const boundary = if (result.exclude_root) result.root else null;
|
||||
while (tw.next()) |node| {
|
||||
if (matches(node, optimized_selector, boundary)) {
|
||||
if (matches(node, optimized_selector, page)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -175,7 +173,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
|
||||
.segments = selector.segments[0 .. seg_idx + 1],
|
||||
};
|
||||
|
||||
if (!matches(id_node, prefix_selector, null)) {
|
||||
if (!matches(id_node, prefix_selector, page)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -250,23 +248,23 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor {
|
||||
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;
|
||||
|
||||
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];
|
||||
if (!matchesCompound(el, last_segment.compound)) {
|
||||
if (!matchesCompound(el, last_segment.compound, page)) {
|
||||
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
|
||||
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 target_compound = if (segment_index == 0)
|
||||
selector.first
|
||||
@@ -274,9 +272,9 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
||||
selector.segments[segment_index - 1].compound;
|
||||
|
||||
const matched: ?*Node = switch (segment.combinator) {
|
||||
.descendant => matchDescendant(node, target_compound, root),
|
||||
.child => matchChild(node, target_compound, root),
|
||||
.next_sibling => matchNextSibling(node, target_compound),
|
||||
.descendant => matchDescendant(node, target_compound, root, page),
|
||||
.child => matchChild(node, target_compound, root, page),
|
||||
.next_sibling => matchNextSibling(node, target_compound, page),
|
||||
.subsequent_sibling => {
|
||||
// For subsequent_sibling, try all matching siblings with backtracking
|
||||
var sibling = node.previousSibling();
|
||||
@@ -286,13 +284,13 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
|
||||
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 (segment_index == 0) {
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
// 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) {
|
||||
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
|
||||
@@ -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)
|
||||
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;
|
||||
|
||||
while (current) |ancestor| {
|
||||
if (ancestor.is(Node.Element)) |ancestor_el| {
|
||||
if (matchesCompound(ancestor_el, compound)) {
|
||||
if (matchesCompound(ancestor_el, compound, page)) {
|
||||
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
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
if (matchesCompound(parent_el, compound)) {
|
||||
if (matchesCompound(parent_el, compound, page)) {
|
||||
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
|
||||
fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
||||
fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
|
||||
var sibling = node.previousSibling();
|
||||
|
||||
// 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
|
||||
if (matchesCompound(sibling_el, compound)) {
|
||||
if (matchesCompound(sibling_el, compound, page)) {
|
||||
return s;
|
||||
}
|
||||
// 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
|
||||
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
||||
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
|
||||
var sibling = node.previousSibling();
|
||||
|
||||
// For subsequent_sibling (~), check all preceding element siblings
|
||||
@@ -396,7 +394,7 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
||||
continue;
|
||||
};
|
||||
|
||||
if (matchesCompound(sibling_el, compound)) {
|
||||
if (matchesCompound(sibling_el, compound, page)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -406,17 +404,17 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node {
|
||||
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.parts) |part| {
|
||||
if (!matchesPart(el, part)) {
|
||||
if (!matchesPart(el, part, page)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn matchesPart(el: *Node.Element, part: Part) bool {
|
||||
fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool {
|
||||
switch (part) {
|
||||
.id => |id| {
|
||||
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);
|
||||
},
|
||||
.universal => return true,
|
||||
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo),
|
||||
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, page),
|
||||
.attribute => |attr| return matchesAttribute(el, attr),
|
||||
}
|
||||
}
|
||||
@@ -497,13 +495,79 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool {
|
||||
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) {
|
||||
// State pseudo-classes
|
||||
.modal => return false,
|
||||
.checked => {
|
||||
const input = el.is(Node.Element.Html.Input) orelse return false;
|
||||
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),
|
||||
.last_child => return 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_of_type => |pattern| return matchesNthOfType(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| {
|
||||
// CSS Level 4: :not() matches if NONE of the selectors match
|
||||
// Each selector in the list is evaluated independently
|
||||
for (selectors) |selector| {
|
||||
if (matches(el.asNode(), selector, null)) {
|
||||
if (matches(node, selector, page)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const node = el.asNode();
|
||||
var sibling = node.previousSibling();
|
||||
|
||||
@@ -395,21 +395,144 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
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;
|
||||
}
|
||||
|
||||
switch (name.len) {
|
||||
4 => {
|
||||
if (fastEql(name, "root")) return .root;
|
||||
if (fastEql(name, "link")) return .link;
|
||||
},
|
||||
5 => {
|
||||
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 => {
|
||||
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 => {
|
||||
if (fastEql(name, "only-child")) return .only_child;
|
||||
if (fastEql(name, "last-child")) return .last_child;
|
||||
if (fastEql(name, "read-write")) return .read_write;
|
||||
},
|
||||
11 => {
|
||||
if (fastEql(name, "first-child")) return .first_child;
|
||||
@@ -417,9 +540,16 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
12 => {
|
||||
if (fastEql(name, "only-of-type")) return .only_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 => {
|
||||
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 => {},
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
|
||||
for (selectors) |selector| {
|
||||
if (List.matches(el.asNode(), selector, null)) {
|
||||
if (List.matches(el.asNode(), selector, page)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -131,8 +131,41 @@ pub const AttributeMatcher = union(enum) {
|
||||
};
|
||||
|
||||
pub const PseudoClass = union(enum) {
|
||||
// State pseudo-classes
|
||||
modal,
|
||||
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,
|
||||
last_child,
|
||||
only_child,
|
||||
@@ -143,7 +176,16 @@ pub const PseudoClass = union(enum) {
|
||||
nth_last_child: NthPattern,
|
||||
nth_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
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user