diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 911c3015..ac732b63 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -33,6 +33,7 @@ const Element = @import("element.zig").Element; const ElementUnion = @import("element.zig").Union; const TreeWalker = @import("tree_walker.zig").TreeWalker; const CSSStyleSheet = @import("../cssom/css_stylesheet.zig").CSSStyleSheet; +const NodeIterator = @import("node_iterator.zig").NodeIterator; const Range = @import("range.zig").Range; const Env = @import("../env.zig").Env; @@ -265,6 +266,10 @@ pub const Document = struct { return try TreeWalker.init(root, what_to_show, filter); } + pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !NodeIterator { + return try NodeIterator.init(root, what_to_show, filter); + } + pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element { if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| { if (state.active_element) |ae| { diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index 13e8a801..8cc6d55e 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -28,6 +28,7 @@ const MutationObserver = @import("mutation_observer.zig"); const IntersectionObserver = @import("intersection_observer.zig"); const DOMParser = @import("dom_parser.zig").DOMParser; const TreeWalker = @import("tree_walker.zig").TreeWalker; +const NodeIterator = @import("node_iterator.zig").NodeIterator; const NodeFilter = @import("node_filter.zig").NodeFilter; const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver; @@ -46,6 +47,7 @@ pub const Interfaces = .{ IntersectionObserver.Interfaces, DOMParser, TreeWalker, + NodeIterator, NodeFilter, @import("performance.zig").Interfaces, PerformanceObserver, diff --git a/src/browser/dom/node_filter.zig b/src/browser/dom/node_filter.zig index c7cc896d..8ba80e10 100644 --- a/src/browser/dom/node_filter.zig +++ b/src/browser/dom/node_filter.zig @@ -22,6 +22,7 @@ pub const NodeFilter = struct { pub const _FILTER_ACCEPT: u16 = 1; pub const _FILTER_REJECT: u16 = 2; pub const _FILTER_SKIP: u16 = 3; + pub const _SHOW_ALL: u32 = std.math.maxInt(u32); pub const _SHOW_ELEMENT: u32 = 0b1; pub const _SHOW_ATTRIBUTE: u32 = 0b10; diff --git a/src/browser/dom/node_iterator.zig b/src/browser/dom/node_iterator.zig new file mode 100644 index 00000000..336368ff --- /dev/null +++ b/src/browser/dom/node_iterator.zig @@ -0,0 +1,213 @@ +// 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 std = @import("std"); +const parser = @import("../netsurf.zig"); +const Env = @import("../env.zig").Env; +const TreeWalker = @import("tree_walker.zig").TreeWalker; + +// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator +pub const NodeIterator = struct { + walker: TreeWalker, + pointer_before_current: bool = true, + + pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !NodeIterator { + return .{ .walker = try TreeWalker.init(node, what_to_show, filter) }; + } + + pub fn get_filter(self: *const NodeIterator) ?Env.Function { + return self.walker.filter; + } + + pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool { + return self.pointer_before_current; + } + + pub fn get_referenceNode(self: *const NodeIterator) *parser.Node { + return self.walker.current_node; + } + + pub fn get_root(self: *const NodeIterator) *parser.Node { + return self.walker.root; + } + + pub fn get_whatToShow(self: *const NodeIterator) u32 { + return self.walker.what_to_show; + } + + pub fn _nextNode(self: *NodeIterator) !?*parser.Node { + if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node + self.pointer_before_current = false; + if (.accept == try self.walker.verify(self.walker.current_node)) { + return self.walker.current_node; + } + } + + if (try self.firstChild(self.walker.current_node)) |child| { + self.walker.current_node = child; + return child; + } + + var current = self.walker.current_node; + while (current != self.walker.root) { + if (try self.walker.nextSibling(current)) |sibling| { + self.walker.current_node = sibling; + return sibling; + } + + current = (try parser.nodeParentNode(current)) orelse break; + } + + return null; + } + + pub fn _previousNode(self: *NodeIterator) !?*parser.Node { + if (!self.pointer_before_current) { + self.pointer_before_current = true; + if (.accept == try self.walker.verify(self.walker.current_node)) { + return self.walker.current_node; // Still need to verify as last may be first as well + } + } + if (self.walker.current_node == self.walker.root) return null; + + var current = self.walker.current_node; + while (try parser.nodePreviousSibling(current)) |previous| { + current = previous; + + switch (try self.walker.verify(current)) { + .accept => { + // Get last child if it has one. + if (try self.lastChild(current)) |child| { + self.walker.current_node = child; + return child; + } + + // Otherwise, this node is our previous one. + self.walker.current_node = current; + return current; + }, + .reject, .skip => { + // Get last child if it has one. + if (try self.lastChild(current)) |child| { + self.walker.current_node = child; + return child; + } + }, + } + } + + if (current != self.walker.root) { + if (try self.walker.parentNode(current)) |parent| { + self.walker.current_node = parent; + return parent; + } + } + + return null; + } + + fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { + const children = try parser.nodeGetChildNodes(node); + const child_count = try parser.nodeListLength(children); + + for (0..child_count) |i| { + const index: u32 = @intCast(i); + const child = (try parser.nodeListItem(children, index)) orelse return null; + + switch (try self.walker.verify(child)) { + .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker + .reject, .skip => if (try self.firstChild(child)) |gchild| return gchild, + } + } + + return null; + } + + fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { + const children = try parser.nodeGetChildNodes(node); + const child_count = try parser.nodeListLength(children); + + var index: u32 = child_count; + while (index > 0) { + index -= 1; + const child = (try parser.nodeListItem(children, index)) orelse return null; + + switch (try self.walker.verify(child)) { + .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker + .reject, .skip => if (try self.lastChild(child)) |gchild| return gchild, + } + } + + return null; + } +}; + +const testing = @import("../../testing.zig"); +test "Browser.DOM.NodeFilter" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ + \\ const nodeIterator = document.createNodeIterator( + \\ document.body, + \\ NodeFilter.SHOW_ELEMENT, + \\ { + \\ acceptNode(node) { + \\ return NodeFilter.FILTER_ACCEPT; + \\ }, + \\ }, + \\ ); + \\ nodeIterator.nextNode().nodeName; + , + "BODY", + }, + .{ "nodeIterator.nextNode().nodeName", "DIV" }, + .{ "nodeIterator.nextNode().nodeName", "A" }, + .{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips + .{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips + .{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips + .{ "nodeIterator.previousNode().nodeName", "DIV" }, + .{ "nodeIterator.previousNode().nodeName", "BODY" }, + .{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root + .{ "nodeIterator.previousNode()", "null" }, // Keeps returning null + .{ "nodeIterator.nextNode().nodeName", "BODY" }, + + .{ "nodeIterator.nextNode().nodeName", null }, + .{ "nodeIterator.nextNode().nodeName", null }, + .{ "nodeIterator.nextNode().nodeName", null }, + .{ "nodeIterator.nextNode().nodeName", "SPAN" }, + .{ "nodeIterator.nextNode().nodeName", "P" }, + .{ "nodeIterator.nextNode()", "null" }, // Just the last one + .{ "nodeIterator.nextNode()", "null" }, // Keeps returning null + .{ "nodeIterator.previousNode().nodeName", "P" }, + }, .{}); + + try runner.testCases(&.{ + .{ + \\ const notationIterator = document.createNodeIterator( + \\ document.body, + \\ NodeFilter.SHOW_NOTATION, + \\ ); + \\ notationIterator.nextNode(); + , + "null", + }, + .{ "notationIterator.previousNode()", "null" }, + }, .{}); +} diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig index a79567b3..55160c3d 100644 --- a/src/browser/dom/tree_walker.zig +++ b/src/browser/dom/tree_walker.zig @@ -55,7 +55,7 @@ pub const TreeWalker = struct { const VerifyResult = enum { accept, skip, reject }; - fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult { + pub fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult { const node_type = try parser.nodeType(node); const what_to_show = self.what_to_show; @@ -77,7 +77,7 @@ pub const TreeWalker = struct { // Verify that we aren't filtering it out. if (self.filter) |f| { - const filter = try f.call(u32, .{node}); + const filter = try f.call(u16, .{node}); return switch (filter) { NodeFilter._FILTER_ACCEPT => .accept, NodeFilter._FILTER_REJECT => .reject, @@ -144,7 +144,7 @@ pub const TreeWalker = struct { return null; } - fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { + pub fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { var current = node; while (true) { @@ -174,7 +174,7 @@ pub const TreeWalker = struct { return null; } - fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { + pub fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { if (self.root == node) return null; var current = node; @@ -245,6 +245,8 @@ pub const TreeWalker = struct { } pub fn _previousNode(self: *TreeWalker) !?*parser.Node { + if (self.current_node == self.root) return null; + var current = self.current_node; while (try parser.nodePreviousSibling(current)) |previous| { current = previous; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index c1be58c6..6ce63027 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -360,7 +360,7 @@ const TimerCallback = struct { } call catch { - log.debug(.user_script, "callback error", .{ + log.warn(.user_script, "callback error", .{ .err = result.exception, .stack = result.stack, .source = "window timeout",