diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index c3cb095d..e20d9b7f 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -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"),
diff --git a/src/browser/tests/element/query_selector_scope.html b/src/browser/tests/element/query_selector_scope.html
new file mode 100644
index 00000000..ba18615a
--- /dev/null
+++ b/src/browser/tests/element/query_selector_scope.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+ Grandchild 1
+ Grandchild 2
+
+
+ Grandchild 3
+
+
+
+
+
+
+
+
+
+
+ Inner text
+
+
+ Other text
+
+
+
+
+
+
+
+
Box 1
+
Box 2
+
Box 3
+
+
+
+
+
+
+
diff --git a/src/browser/tests/media/mediaerror.html b/src/browser/tests/media/mediaerror.html
new file mode 100644
index 00000000..928860fb
--- /dev/null
+++ b/src/browser/tests/media/mediaerror.html
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/src/browser/webapi/media/MediaError.zig b/src/browser/webapi/media/MediaError.zig
new file mode 100644
index 00000000..5e1f15b4
--- /dev/null
+++ b/src/browser/webapi/media/MediaError.zig
@@ -0,0 +1,64 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+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", .{});
+}
diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig
index 8ce759a8..06d2045e 100644
--- a/src/browser/webapi/selector/List.zig
+++ b/src/browser/webapi/selector/List.zig
@@ -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;
}
diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig
index a793e7c8..02d9e1c7 100644
--- a/src/browser/webapi/selector/Parser.zig
+++ b/src/browser/webapi/selector/Parser.zig
@@ -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;
},
diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig
index 5360cd3f..44d7c438 100644
--- a/src/browser/webapi/selector/Selector.zig
+++ b/src/browser/webapi/selector/Selector.zig
@@ -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,