Skip to content

Commit cf5815e

Browse files
committed
NodeIterator
1 parent 6b41677 commit cf5815e

File tree

6 files changed

+228
-5
lines changed

6 files changed

+228
-5
lines changed

src/browser/dom/document.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const css = @import("css.zig");
3232
const Element = @import("element.zig").Element;
3333
const ElementUnion = @import("element.zig").Union;
3434
const TreeWalker = @import("tree_walker.zig").TreeWalker;
35+
const NodeIterator = @import("node_iterator.zig").NodeIterator;
3536
const Range = @import("range.zig").Range;
3637

3738
const Env = @import("../env.zig").Env;
@@ -264,6 +265,10 @@ pub const Document = struct {
264265
return try TreeWalker.init(root, what_to_show, filter);
265266
}
266267

268+
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !NodeIterator {
269+
return try NodeIterator.init(root, what_to_show, filter);
270+
}
271+
267272
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
268273
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
269274
if (state.active_element) |ae| {

src/browser/dom/dom.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const MutationObserver = @import("mutation_observer.zig");
2727
const IntersectionObserver = @import("intersection_observer.zig");
2828
const DOMParser = @import("dom_parser.zig").DOMParser;
2929
const TreeWalker = @import("tree_walker.zig").TreeWalker;
30+
const NodeIterator = @import("node_iterator.zig").NodeIterator;
3031
const NodeFilter = @import("node_filter.zig").NodeFilter;
3132
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
3233

@@ -44,6 +45,7 @@ pub const Interfaces = .{
4445
IntersectionObserver.Interfaces,
4546
DOMParser,
4647
TreeWalker,
48+
NodeIterator,
4749
NodeFilter,
4850
@import("performance.zig").Interfaces,
4951
PerformanceObserver,

src/browser/dom/node_filter.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub const NodeFilter = struct {
2222
pub const _FILTER_ACCEPT: u16 = 1;
2323
pub const _FILTER_REJECT: u16 = 2;
2424
pub const _FILTER_SKIP: u16 = 3;
25+
2526
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
2627
pub const _SHOW_ELEMENT: u32 = 0b1;
2728
pub const _SHOW_ATTRIBUTE: u32 = 0b10;

src/browser/dom/node_iterator.zig

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const parser = @import("../netsurf.zig");
21+
const Env = @import("../env.zig").Env;
22+
const TreeWalker = @import("tree_walker.zig").TreeWalker;
23+
24+
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
25+
pub const NodeIterator = struct {
26+
walker: TreeWalker,
27+
pointer_before_current: bool = true,
28+
29+
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !NodeIterator {
30+
return .{ .walker = try TreeWalker.init(node, what_to_show, filter) };
31+
}
32+
33+
pub fn get_filter(self: *const NodeIterator) ?Env.Function {
34+
return self.walker.filter;
35+
}
36+
37+
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
38+
return self.pointer_before_current;
39+
}
40+
41+
pub fn get_referenceNode(self: *const NodeIterator) *parser.Node {
42+
return self.walker.current_node;
43+
}
44+
45+
pub fn get_root(self: *const NodeIterator) *parser.Node {
46+
return self.walker.root;
47+
}
48+
49+
pub fn get_whatToShow(self: *const NodeIterator) u32 {
50+
return self.walker.what_to_show;
51+
}
52+
53+
pub fn _nextNode(self: *NodeIterator) !?*parser.Node {
54+
if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node
55+
self.pointer_before_current = false;
56+
if (.accept == try self.walker.verify(self.walker.current_node)) {
57+
return self.walker.current_node;
58+
}
59+
}
60+
61+
if (try self.firstChild(self.walker.current_node)) |child| {
62+
self.walker.current_node = child;
63+
return child;
64+
}
65+
66+
var current = self.walker.current_node;
67+
while (current != self.walker.root) {
68+
if (try self.walker.nextSibling(current)) |sibling| {
69+
self.walker.current_node = sibling;
70+
return sibling;
71+
}
72+
73+
current = (try parser.nodeParentNode(current)) orelse break;
74+
}
75+
76+
return null;
77+
}
78+
79+
pub fn _previousNode(self: *NodeIterator) !?*parser.Node {
80+
if (!self.pointer_before_current) {
81+
self.pointer_before_current = true;
82+
if (.accept == try self.walker.verify(self.walker.current_node)) {
83+
return self.walker.current_node; // Still need to verify as last may be first as well
84+
}
85+
}
86+
if (self.walker.current_node == self.walker.root) return null;
87+
88+
var current = self.walker.current_node;
89+
while (try parser.nodePreviousSibling(current)) |previous| {
90+
current = previous;
91+
92+
switch (try self.walker.verify(current)) {
93+
.accept => {
94+
// Get last child if it has one.
95+
if (try self.lastChild(current)) |child| {
96+
self.walker.current_node = child;
97+
return child;
98+
}
99+
100+
// Otherwise, this node is our previous one.
101+
self.walker.current_node = current;
102+
return current;
103+
},
104+
.reject, .skip => {
105+
// Get last child if it has one.
106+
if (try self.lastChild(current)) |child| {
107+
self.walker.current_node = child;
108+
return child;
109+
}
110+
},
111+
}
112+
}
113+
114+
if (current != self.walker.root) {
115+
if (try self.walker.parentNode(current)) |parent| {
116+
self.walker.current_node = parent;
117+
return parent;
118+
}
119+
}
120+
121+
return null;
122+
}
123+
124+
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
125+
const children = try parser.nodeGetChildNodes(node);
126+
const child_count = try parser.nodeListLength(children);
127+
128+
for (0..child_count) |i| {
129+
const index: u32 = @intCast(i);
130+
const child = (try parser.nodeListItem(children, index)) orelse return null;
131+
132+
switch (try self.walker.verify(child)) {
133+
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
134+
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
135+
}
136+
}
137+
138+
return null;
139+
}
140+
141+
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
142+
const children = try parser.nodeGetChildNodes(node);
143+
const child_count = try parser.nodeListLength(children);
144+
145+
var index: u32 = child_count;
146+
while (index > 0) {
147+
index -= 1;
148+
const child = (try parser.nodeListItem(children, index)) orelse return null;
149+
150+
switch (try self.walker.verify(child)) {
151+
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
152+
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
153+
}
154+
}
155+
156+
return null;
157+
}
158+
};
159+
160+
const testing = @import("../../testing.zig");
161+
test "Browser.DOM.NodeFilter" {
162+
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
163+
defer runner.deinit();
164+
165+
try runner.testCases(&.{
166+
.{
167+
\\ const nodeIterator = document.createNodeIterator(
168+
\\ document.body,
169+
\\ NodeFilter.SHOW_ELEMENT,
170+
\\ {
171+
\\ acceptNode(node) {
172+
\\ return NodeFilter.FILTER_ACCEPT;
173+
\\ },
174+
\\ },
175+
\\ );
176+
\\ nodeIterator.nextNode().nodeName;
177+
,
178+
"BODY",
179+
},
180+
.{ "nodeIterator.nextNode().nodeName", "DIV" },
181+
.{ "nodeIterator.nextNode().nodeName", "A" },
182+
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
183+
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
184+
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
185+
.{ "nodeIterator.previousNode().nodeName", "DIV" },
186+
.{ "nodeIterator.previousNode().nodeName", "BODY" },
187+
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
188+
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
189+
.{ "nodeIterator.nextNode().nodeName", "BODY" },
190+
191+
.{ "nodeIterator.nextNode().nodeName", null },
192+
.{ "nodeIterator.nextNode().nodeName", null },
193+
.{ "nodeIterator.nextNode().nodeName", null },
194+
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
195+
.{ "nodeIterator.nextNode().nodeName", "P" },
196+
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
197+
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
198+
.{ "nodeIterator.previousNode().nodeName", "P" },
199+
}, .{});
200+
201+
try runner.testCases(&.{
202+
.{
203+
\\ const notationIterator = document.createNodeIterator(
204+
\\ document.body,
205+
\\ NodeFilter.SHOW_NOTATION,
206+
\\ );
207+
\\ notationIterator.nextNode();
208+
,
209+
"null",
210+
},
211+
.{ "notationIterator.previousNode()", "null" },
212+
}, .{});
213+
}

src/browser/dom/tree_walker.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub const TreeWalker = struct {
5555

5656
const VerifyResult = enum { accept, skip, reject };
5757

58-
fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
58+
pub fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
5959
const node_type = try parser.nodeType(node);
6060
const what_to_show = self.what_to_show;
6161

@@ -77,7 +77,7 @@ pub const TreeWalker = struct {
7777

7878
// Verify that we aren't filtering it out.
7979
if (self.filter) |f| {
80-
const filter = try f.call(u32, .{node});
80+
const filter = try f.call(u16, .{node});
8181
return switch (filter) {
8282
NodeFilter._FILTER_ACCEPT => .accept,
8383
NodeFilter._FILTER_REJECT => .reject,
@@ -144,7 +144,7 @@ pub const TreeWalker = struct {
144144
return null;
145145
}
146146

147-
fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
147+
pub fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
148148
var current = node;
149149

150150
while (true) {
@@ -174,7 +174,7 @@ pub const TreeWalker = struct {
174174
return null;
175175
}
176176

177-
fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
177+
pub fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
178178
if (self.root == node) return null;
179179

180180
var current = node;
@@ -245,6 +245,8 @@ pub const TreeWalker = struct {
245245
}
246246

247247
pub fn _previousNode(self: *TreeWalker) !?*parser.Node {
248+
if (self.current_node == self.root) return null;
249+
248250
var current = self.current_node;
249251
while (try parser.nodePreviousSibling(current)) |previous| {
250252
current = previous;

src/browser/html/window.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ const TimerCallback = struct {
360360
}
361361

362362
call catch {
363-
log.debug(.user_script, "callback error", .{
363+
log.warn(.user_script, "callback error", .{
364364
.err = result.exception,
365365
.stack = result.stack,
366366
.source = "window timeout",

0 commit comments

Comments
 (0)