MediaError and :scope pseudoclass

This commit is contained in:
Karl Seguin
2025-12-03 22:29:45 +08:00
parent 60c1f19581
commit 7cb06f3e58
7 changed files with 248 additions and 37 deletions

View File

@@ -565,6 +565,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),
@import("../webapi/media/TextTrackCue.zig"),
@import("../webapi/media/VTTCue.zig"),
@import("../webapi/EventTarget.zig"),

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container">
<div id="child1" class="item">
<span id="grandchild1">Grandchild 1</span>
<span id="grandchild2">Grandchild 2</span>
</div>
<div id="child2" class="item">
<span id="grandchild3">Grandchild 3</span>
</div>
</div>
<script id="scopeBasic">
{
// :scope refers to the reference element but querySelector only returns descendants
const container = $('#container');
// :scope alone doesn't match anything because querySelector only returns descendants
const scopeMatch = container.querySelector(':scope');
testing.expectEqual(null, scopeMatch);
// :scope in querySelectorAll should also return empty
const scopeMatches = container.querySelectorAll(':scope');
testing.expectEqual(0, scopeMatches.length);
}
</script>
<script id="scopeWithCombinators">
{
const container = $('#container');
// :scope > child - direct children of scope
const directChildren = container.querySelectorAll(':scope > div');
testing.expectEqual(2, directChildren.length);
testing.expectEqual($('#child1'), directChildren[0]);
testing.expectEqual($('#child2'), directChildren[1]);
// :scope > .item - direct children with class
const itemChildren = container.querySelectorAll(':scope > .item');
testing.expectEqual(2, itemChildren.length);
// :scope span - descendant spans of scope
const spans = container.querySelectorAll(':scope span');
testing.expectEqual(3, spans.length);
// :scope > div > span - grandchildren via specific path
const grandchildren = container.querySelectorAll(':scope > div > span');
testing.expectEqual(3, grandchildren.length);
}
</script>
<div id="nested-container">
<div class="outer">
<div class="inner" id="target">
<span class="text">Inner text</span>
</div>
<div class="inner">
<span class="text">Other text</span>
</div>
</div>
</div>
<script id="scopeNested">
{
const target = $('#target');
// :scope refers to target but querySelector only returns descendants
const scopeMatch = target.querySelector(':scope');
testing.expectEqual(null, scopeMatch);
// :scope > span should find direct children of target
const directSpan = target.querySelector(':scope > span');
testing.expectEqual('Inner text', directSpan.textContent);
// When querySelector is called on target, :scope refers to target
const scopeChildren = target.querySelectorAll(':scope > .text');
testing.expectEqual(1, scopeChildren.length);
}
</script>
<div id="compound-test">
<div class="box" id="box1">Box 1</div>
<div class="box" id="box2">Box 2</div>
<span class="box" id="box3">Box 3</span>
</div>
<script id="scopeCompound">
{
const compound = $('#compound-test');
// Compound selector with :scope
const divBoxes = compound.querySelectorAll(':scope > div.box');
testing.expectEqual(2, divBoxes.length);
testing.expectEqual($('#box1'), divBoxes[0]);
testing.expectEqual($('#box2'), divBoxes[1]);
// :scope with multiple parts
const spanBox = compound.querySelector(':scope > span.box');
testing.expectEqual($('#box3'), spanBox);
}
</script>
<div id="pseudo-container">
<div class="parent" id="p1">
<div class="child">Child 1</div>
<div class="child">Child 2</div>
</div>
<div class="parent" id="p2">
<div class="child">Child 3</div>
</div>
</div>
<script id="scopeWithOtherPseudos">
{
const container = $('#pseudo-container');
// :scope with :not()
const notP1Children = container.querySelectorAll(':scope > .parent:not(#p1) > .child');
testing.expectEqual(1, notP1Children.length);
testing.expectEqual('Child 3', notP1Children[0].textContent);
// :scope with :first-child
const firstParent = container.querySelector(':scope > .parent:first-child');
testing.expectEqual($('#p1'), firstParent);
}
</script>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<script src="..//testing.js"></script>
<script id=constants>
{
// Test that MediaError constants exist
testing.expectEqual(1, MediaError.MEDIA_ERR_ABORTED);
testing.expectEqual(2, MediaError.MEDIA_ERR_NETWORK);
testing.expectEqual(3, MediaError.MEDIA_ERR_DECODE);
testing.expectEqual(4, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
}
</script>

View File

@@ -0,0 +1,64 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const MediaError = @This();
_code: u16,
_message: []const u8 = "",
pub fn init(code: u16, message: []const u8, page: *Page) !*MediaError {
return page.arena.create(MediaError{
._code = code,
._message = try page.dupeString(message),
});
}
pub fn getCode(self: *const MediaError) u16 {
return self._code;
}
pub fn getMessage(self: *const MediaError) []const u8 {
return self._message;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(MediaError);
pub const Meta = struct {
pub const name = "MediaError";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
// Error code constants
pub const MEDIA_ERR_ABORTED = bridge.property(1);
pub const MEDIA_ERR_NETWORK = bridge.property(2);
pub const MEDIA_ERR_DECODE = bridge.property(3);
pub const MEDIA_ERR_SRC_NOT_SUPPORTED = bridge.property(4);
pub const code = bridge.accessor(MediaError.getCode, null, .{});
pub const message = bridge.accessor(MediaError.getMessage, null, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: MediaError" {
try testing.htmlRunner("media/mediaerror.html", .{});
}

View File

@@ -54,7 +54,7 @@ pub fn collect(
}
while (tw.next()) |node| {
if (matches(node, result.selector, page)) {
if (matches(node, result.selector, root, page)) {
try nodes.put(allocator, node, {});
}
}
@@ -66,12 +66,11 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node {
const result = optimizeSelector(root, &selector, page) orelse return null;
var tw = TreeWalker.init(result.root, .{});
const optimized_selector = result.selector;
if (result.exclude_root) {
_ = tw.next();
}
while (tw.next()) |node| {
if (matches(node, optimized_selector, page)) {
if (matches(node, result.selector, root, page)) {
return node;
}
}
@@ -89,10 +88,12 @@ const OptimizeResult = struct {
exclude_root: bool,
selector: Selector.Selector,
};
fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page) ?OptimizeResult {
const anchor = findIdSelector(selector) orelse return .{
.root = root,
.selector = selector.*,
// Always exclude root - querySelector only returns descendants
.exclude_root = true,
};
@@ -173,7 +174,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
.segments = selector.segments[0 .. seg_idx + 1],
};
if (!matches(id_node, prefix_selector, page)) {
if (!matches(id_node, prefix_selector, id_node, page)) {
return null;
}
@@ -248,23 +249,23 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor {
return null;
}
pub fn matches(node: *Node, selector: Selector.Selector, page: *Page) bool {
pub fn matches(node: *Node, selector: Selector.Selector, scope: *Node, page: *Page) bool {
const el = node.is(Node.Element) orelse return false;
if (selector.segments.len == 0) {
return matchesCompound(el, selector.first, page);
return matchesCompound(el, selector.first, scope, page);
}
const last_segment = selector.segments[selector.segments.len - 1];
if (!matchesCompound(el, last_segment.compound, page)) {
if (!matchesCompound(el, last_segment.compound, scope, page)) {
return false;
}
return matchSegments(node, selector, selector.segments.len - 1, null, page);
return matchSegments(node, selector, selector.segments.len - 1, null, scope, page);
}
// Match segments backward, with support for backtracking on subsequent_sibling
fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, page: *Page) bool {
fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, scope: *Node, page: *Page) bool {
const segment = selector.segments[segment_index];
const target_compound = if (segment_index == 0)
selector.first
@@ -272,9 +273,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, page),
.child => matchChild(node, target_compound, root, page),
.next_sibling => matchNextSibling(node, target_compound, page),
.descendant => matchDescendant(node, target_compound, root, scope, page),
.child => matchChild(node, target_compound, root, scope, page),
.next_sibling => matchNextSibling(node, target_compound, scope, page),
.subsequent_sibling => {
// For subsequent_sibling, try all matching siblings with backtracking
var sibling = node.previousSibling();
@@ -284,13 +285,13 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize,
continue;
};
if (matchesCompound(sibling_el, target_compound, page)) {
if (matchesCompound(sibling_el, target_compound, scope, 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, page)) {
if (matchSegments(s, selector, segment_index - 1, root, scope, page)) {
return true;
}
// This sibling didn't work, try the next one
@@ -307,7 +308,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, page);
return matchSegments(current, selector, segment_index - 1, root, scope, page);
}
// subsequent_sibling already handled its recursion above
@@ -315,12 +316,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, page: *Page) ?*Node {
fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node {
var current = node._parent;
while (current) |ancestor| {
if (ancestor.is(Node.Element)) |ancestor_el| {
if (matchesCompound(ancestor_el, compound, page)) {
if (matchesCompound(ancestor_el, compound, scope, page)) {
return ancestor;
}
}
@@ -339,7 +340,7 @@ fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, page:
}
// Find the direct parent if it matches the compound
fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node {
fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node {
const parent = node._parent orelse return null;
// Don't match beyond the root boundary
@@ -352,7 +353,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Pag
const parent_el = parent.is(Node.Element) orelse return null;
if (matchesCompound(parent_el, compound, page)) {
if (matchesCompound(parent_el, compound, scope, page)) {
return parent;
}
@@ -360,7 +361,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Pag
}
// Find the immediately preceding sibling if it matches the compound
fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
fn matchNextSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node {
var sibling = node.previousSibling();
// For next_sibling (+), we need the immediately preceding element sibling
@@ -372,7 +373,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Nod
};
// Found an element - check if it matches
if (matchesCompound(sibling_el, compound, page)) {
if (matchesCompound(sibling_el, compound, scope, page)) {
return s;
}
// we found an element, it wasn't a match, we're done
@@ -383,7 +384,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Nod
}
// Find any preceding sibling that matches the compound
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node {
fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node {
var sibling = node.previousSibling();
// For subsequent_sibling (~), check all preceding element siblings
@@ -394,7 +395,7 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page)
continue;
};
if (matchesCompound(sibling_el, compound, page)) {
if (matchesCompound(sibling_el, compound, scope, page)) {
return s;
}
@@ -404,17 +405,17 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page)
return null;
}
fn matchesCompound(el: *Node.Element, compound: Selector.Compound, page: *Page) bool {
fn matchesCompound(el: *Node.Element, compound: Selector.Compound, scope: *Node, page: *Page) bool {
// For compound selectors, ALL parts must match
for (compound.parts) |part| {
if (!matchesPart(el, part, page)) {
if (!matchesPart(el, part, scope, page)) {
return false;
}
}
return true;
}
fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool {
fn matchesPart(el: *Node.Element, part: Part, scope: *Node, page: *Page) bool {
switch (part) {
.id => |id| {
const element_id = el.getAttributeSafe("id") orelse return false;
@@ -435,7 +436,7 @@ fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool {
return std.mem.eql(u8, element_tag, tag_name);
},
.universal => return true,
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, page),
.pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, scope, page),
.attribute => |attr| return matchesAttribute(el, attr),
}
}
@@ -495,7 +496,7 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool {
return false;
}
fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Page) bool {
fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *Node, page: *Page) bool {
const node = el.asNode();
switch (pseudo) {
// State pseudo-classes
@@ -565,6 +566,10 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
const parent = node.parentNode() orelse return false;
return parent._type == .document;
},
.scope => {
// :scope matches the reference element (querySelector root)
return node == scope;
},
.empty => {
return node.firstChild() == null;
},
@@ -591,7 +596,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
.lang => return false,
.not => |selectors| {
for (selectors) |selector| {
if (matches(node, selector, page)) {
if (matches(node, selector, scope, page)) {
return false;
}
}
@@ -599,7 +604,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
},
.is => |selectors| {
for (selectors) |selector| {
if (matches(node, selector, page)) {
if (matches(node, selector, scope, page)) {
return true;
}
}
@@ -607,7 +612,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
},
.where => |selectors| {
for (selectors) |selector| {
if (matches(node, selector, page)) {
if (matches(node, selector, scope, page)) {
return true;
}
}
@@ -622,11 +627,11 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
continue;
};
if (matches(child_el.asNode(), selector, page)) {
if (matches(child_el.asNode(), selector, scope, page)) {
return true;
}
if (matchesHasDescendant(child_el, selector, page)) {
if (matchesHasDescendant(child_el, selector, scope, page)) {
return true;
}
@@ -638,7 +643,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa
}
}
fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *Page) bool {
fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, scope: *Node, page: *Page) bool {
var child = el.asNode().firstChild();
while (child) |c| {
const child_el = c.is(Node.Element) orelse {
@@ -646,11 +651,11 @@ fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *P
continue;
};
if (matches(child_el.asNode(), selector, page)) {
if (matches(child_el.asNode(), selector, scope, page)) {
return true;
}
if (matchesHasDescendant(child_el, selector, page)) {
if (matchesHasDescendant(child_el, selector, scope, page)) {
return true;
}

View File

@@ -504,6 +504,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
if (fastEql(name, "modal")) return .modal;
if (fastEql(name, "hover")) return .hover;
if (fastEql(name, "focus")) return .focus;
if (fastEql(name, "scope")) return .scope;
if (fastEql(name, "empty")) return .empty;
if (fastEql(name, "valid")) return .valid;
},

View File

@@ -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, page)) {
if (List.matches(el.asNode(), selector, el.asNode(), page)) {
return true;
}
}
@@ -165,6 +165,7 @@ pub const PseudoClass = union(enum) {
// Tree structural
root,
scope,
empty,
first_child,
last_child,