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 +
+ + + +
+
+
Child 1
+
Child 2
+
+
+
Child 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,