From 152d0fdda7df4cad72fdabca9a7b4bd598966412 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 19 May 2025 07:17:01 -0700 Subject: [PATCH 1/5] add TreeWalker --- src/browser/dom/document.zig | 7 +++ src/browser/dom/dom.zig | 2 + src/browser/dom/tree_walker.zig | 94 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/browser/dom/tree_walker.zig diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index e3bf4f6b..db48497d 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -30,6 +30,9 @@ const css = @import("css.zig"); const Element = @import("element.zig").Element; const ElementUnion = @import("element.zig").Union; +const TreeWalker = @import("tree_walker.zig").TreeWalker; + +const Env = @import("../env.zig").Env; const DOMImplementation = @import("implementation.zig").DOMImplementation; @@ -238,6 +241,10 @@ pub const Document = struct { pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void { return Node.replaceChildren(parser.documentToNode(self), nodes); } + + pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?Env.Callback) TreeWalker { + return TreeWalker.init(root, what_to_show, filter); + } }; const testing = @import("../../testing.zig"); diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index 2cfb521d..b6029392 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -26,6 +26,7 @@ const Node = @import("node.zig"); 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; pub const Interfaces = .{ DOMException, @@ -39,4 +40,5 @@ pub const Interfaces = .{ MutationObserver.Interfaces, IntersectionObserver.Interfaces, DOMParser, + TreeWalker, }; diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig new file mode 100644 index 00000000..f14164dc --- /dev/null +++ b/src/browser/dom/tree_walker.zig @@ -0,0 +1,94 @@ +// Copyright (C) 2023-2024 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 NodeFilter = @import("node_filter.zig").NodeFilter; +const Env = @import("../env.zig").Env; + +// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker +pub const TreeWalker = struct { + root: *parser.Node, + current_node: *parser.Node, + what_to_show: u32, + filter: ?Env.Callback, + + pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?Env.Callback) TreeWalker { + return .{ + .root = node, + .current_node = node, + .what_to_show = what_to_show orelse NodeFilter._SHOW_ALL, + .filter = filter, + }; + } + + pub fn get_root(self: *TreeWalker) *parser.Node { + return self.root; + } + + pub fn get_currentNode(self: *TreeWalker) *parser.Node { + return self.current_node; + } + + pub fn get_whatToShow(self: *TreeWalker) u32 { + return self.what_to_show; + } + + pub fn get_filter(self: *TreeWalker) ?Env.Callback { + return self.filter; + } + + pub fn _firstChild(self: *TreeWalker) ?*parser.Node { + const first_child = parser.nodeFirstChild(self.current_node) catch return null; + self.current_node = first_child orelse return null; + return first_child; + } + + pub fn _lastChild(self: *TreeWalker) ?*parser.Node { + const last_child = parser.nodeLastChild(self.current_node) catch return null; + self.current_node = last_child orelse return null; + return last_child; + } + + pub fn _nextNode(self: *TreeWalker) ?*parser.Node { + return self._firstChild(); + } + + pub fn _nextSibling(self: *TreeWalker) ?*parser.Node { + const next_sibling = parser.nodeNextSibling(self.current_node) catch return null; + self.current_node = next_sibling orelse return null; + return next_sibling; + } + + pub fn _parentNode(self: *TreeWalker) ?*parser.Node { + const parent = parser.nodeParentNode(self.current_node) catch return null; + self.current_node = parent orelse return null; + return parent; + } + + pub fn _previousNode(self: *TreeWalker) ?*parser.Node { + return self._parentNode(); + } + + pub fn _previousSibling(self: *TreeWalker) ?*parser.Node { + const previous_sibling = parser.nodePreviousSibling(self.current_node) catch return null; + self.current_node = previous_sibling orelse return null; + return previous_sibling; + } +}; From fada732b33b937f66097eb7c5c355d1453315cfb Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 19 May 2025 07:33:24 -0700 Subject: [PATCH 2/5] add NodeFilter --- src/browser/dom/dom.zig | 2 ++ src/browser/dom/node_filter.zig | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/browser/dom/node_filter.zig diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index b6029392..0b998063 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -27,6 +27,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 NodeFilter = @import("node_filter.zig").NodeFilter; pub const Interfaces = .{ DOMException, @@ -41,4 +42,5 @@ pub const Interfaces = .{ IntersectionObserver.Interfaces, DOMParser, TreeWalker, + NodeFilter, }; diff --git a/src/browser/dom/node_filter.zig b/src/browser/dom/node_filter.zig new file mode 100644 index 00000000..c7cc896d --- /dev/null +++ b/src/browser/dom/node_filter.zig @@ -0,0 +1,52 @@ +// Copyright (C) 2023-2024 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"); + +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; + pub const _SHOW_TEXT: u32 = 0b100; + pub const _SHOW_CDATA_SECTION: u32 = 0b1000; + pub const _SHOW_ENTITY_REFERENCE: u32 = 0b10000; + pub const _SHOW_ENTITY: u32 = 0b100000; + pub const _SHOW_PROCESSING_INSTRUCTION: u32 = 0b1000000; + pub const _SHOW_COMMENT: u32 = 0b10000000; + pub const _SHOW_DOCUMENT: u32 = 0b100000000; + pub const _SHOW_DOCUMENT_TYPE: u32 = 0b1000000000; + pub const _SHOW_DOCUMENT_FRAGMENT: u32 = 0b10000000000; + pub const _SHOW_NOTATION: u32 = 0b100000000000; +}; + +const testing = @import("../../testing.zig"); +test "Browser.DOM.NodeFilter" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "NodeFilter.FILTER_ACCEPT", "1" }, + .{ "NodeFilter.FILTER_REJECT", "2" }, + .{ "NodeFilter.FILTER_SKIP", "3" }, + .{ "NodeFilter.SHOW_ALL", "4294967295" }, + .{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" }, + }, .{}); +} From 3c3de9d3252ee29a1a5513f353034aeb117d5f1c Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 21 May 2025 11:26:41 -0700 Subject: [PATCH 3/5] use Env.Function instead of Env.Callback --- src/browser/dom/document.zig | 4 +- src/browser/dom/event_target.zig | 4 +- src/browser/dom/intersection_observer.zig | 8 +- src/browser/dom/mutation_observer.zig | 8 +- src/browser/dom/nodelist.zig | 8 +- src/browser/dom/token_list.zig | 8 +- src/browser/dom/tree_walker.zig | 143 ++++++++++++++++++---- src/browser/env.zig | 2 +- src/browser/events/event.zig | 10 +- src/browser/html/media_query_list.zig | 6 +- src/browser/html/window.zig | 20 +-- src/browser/xhr/event_target.zig | 40 +++--- src/runtime/js.zig | 40 +++--- 13 files changed, 199 insertions(+), 102 deletions(-) diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index db48497d..03d15616 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -242,8 +242,8 @@ pub const Document = struct { return Node.replaceChildren(parser.documentToNode(self), nodes); } - pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?Env.Callback) TreeWalker { - return TreeWalker.init(root, what_to_show, filter); + pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker { + return try TreeWalker.init(root, what_to_show, filter); } }; diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index 33f61395..f25c7e76 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -57,7 +57,7 @@ pub const EventTarget = struct { pub fn _addEventListener( self: *parser.EventTarget, typ: []const u8, - cbk: Env.Callback, + cbk: Env.Function, opts_: ?AddEventListenerOpts, state: *SessionState, ) !void { @@ -104,7 +104,7 @@ pub const EventTarget = struct { pub fn _removeEventListener( self: *parser.EventTarget, typ: []const u8, - cbk: Env.Callback, + cbk: Env.Function, capture: ?bool, // TODO: hanle EventListenerOptions // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 diff --git a/src/browser/dom/intersection_observer.zig b/src/browser/dom/intersection_observer.zig index 0608b70d..6510087a 100644 --- a/src/browser/dom/intersection_observer.zig +++ b/src/browser/dom/intersection_observer.zig @@ -40,7 +40,7 @@ const log = std.log.scoped(.events); // The returned Entries are phony, they always indicate full intersection. // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver pub const IntersectionObserver = struct { - callback: Env.Callback, + callback: Env.Function, options: IntersectionObserverOptions, state: *SessionState, @@ -48,7 +48,7 @@ pub const IntersectionObserver = struct { // new IntersectionObserver(callback) // new IntersectionObserver(callback, options) [not supported yet] - pub fn constructor(callback: Env.Callback, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver { + pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver { var options = IntersectionObserverOptions{ .root = parser.documentToNode(parser.documentHTMLToDocument(state.window.document)), .rootMargin = "0px 0px 0px 0px", @@ -85,8 +85,8 @@ pub const IntersectionObserver = struct { .options = &self.options, }); - var result: Env.Callback.Result = undefined; - self.callback.tryCall(.{self.observed_entries.items}, &result) catch { + var result: Env.Function.Result = undefined; + self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch { log.err("intersection observer callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 71459e87..a7b05a89 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -36,14 +36,14 @@ const log = std.log.scoped(.events); // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver pub const MutationObserver = struct { - cbk: Env.Callback, + cbk: Env.Function, arena: Allocator, // List of records which were observed. When the scopeEnds, we need to // execute our callback with it. observed: std.ArrayListUnmanaged(*MutationRecord), - pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver { + pub fn constructor(cbk: Env.Function, state: *SessionState) !MutationObserver { return .{ .cbk = cbk, .observed = .{}, @@ -113,8 +113,8 @@ pub const MutationObserver = struct { for (record) |r| { const records = [_]MutationRecord{r.*}; - var result: Env.Callback.Result = undefined; - self.cbk.tryCall(.{records}, &result) catch { + var result: Env.Function.Result = undefined; + self.cbk.tryCall(void, .{records}, &result) catch { log.err("mutation observer callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index ca09723b..56cb3e47 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -21,7 +21,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const JsThis = @import("../env.zig").JsThis; -const Callback = @import("../env.zig").Callback; +const Function = @import("../env.zig").Function; const NodeUnion = @import("node.zig").Union; const Node = @import("node.zig").Node; @@ -141,11 +141,11 @@ pub const NodeList = struct { // }; // } - pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg + pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg for (self.nodes.items, 0..) |n, i| { const ii: u32 = @intCast(i); - var result: Callback.Result = undefined; - cbk.tryCall(.{ n, ii, self }, &result) catch { + var result: Function.Result = undefined; + cbk.tryCall(void, .{ n, ii, self }, &result) catch { log.err("callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index c8b900db..83e48256 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -21,7 +21,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const iterator = @import("../iterator/iterator.zig"); -const Callback = @import("../env.zig").Callback; +const Function = @import("../env.zig").Function; const JsObject = @import("../env.zig").JsObject; const DOMException = @import("exceptions.zig").DOMException; @@ -138,11 +138,11 @@ pub const DOMTokenList = struct { } // TODO handle thisArg - pub fn _forEach(self: *parser.TokenList, cbk: Callback, this_arg: JsObject) !void { + pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void { var entries = _entries(self); while (try entries._next()) |entry| { - var result: Callback.Result = undefined; - cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { + var result: Function.Result = undefined; + cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { log.err("callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig index f14164dc..792c0a2a 100644 --- a/src/browser/dom/tree_walker.zig +++ b/src/browser/dom/tree_walker.zig @@ -27,17 +27,63 @@ pub const TreeWalker = struct { root: *parser.Node, current_node: *parser.Node, what_to_show: u32, - filter: ?Env.Callback, + filter: ?Env.Function, + + depth: usize, + + pub const TreeWalkerOpts = union(enum) { + function: Env.Function, + object: struct { acceptNode: Env.Function }, + }; + + pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalkerOpts) !TreeWalker { + var filter_func: ?Env.Function = null; + + if (filter) |f| { + filter_func = switch (f) { + .function => |func| func, + .object => |o| o.acceptNode, + }; + } - pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?Env.Callback) TreeWalker { return .{ .root = node, .current_node = node, .what_to_show = what_to_show orelse NodeFilter._SHOW_ALL, - .filter = filter, + .filter = filter_func, + .depth = 0, }; } + fn verify_what_to_show(self: *const TreeWalker, node: *parser.Node) !bool { + const node_type = try parser.nodeType(node); + const what_to_show = self.what_to_show; + return switch (node_type) { + .attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0, + .cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0, + .comment => what_to_show & NodeFilter._SHOW_COMMENT != 0, + .document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0, + .document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0, + .document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0, + .element => what_to_show & NodeFilter._SHOW_ELEMENT != 0, + .entity => what_to_show & NodeFilter._SHOW_ENTITY != 0, + .entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0, + .notation => what_to_show & NodeFilter._SHOW_NOTATION != 0, + .processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0, + .text => what_to_show & NodeFilter._SHOW_TEXT != 0, + }; + } + + fn verify_filter(self: *const TreeWalker, node: *parser.Node) !bool { + if (self.filter) |f| { + const filter = try f.call(u32, .{node}); + return switch (filter) { + NodeFilter._FILTER_ACCEPT => true, + else => false, + }; + } else return true; + } + pub fn get_root(self: *TreeWalker) *parser.Node { return self.root; } @@ -50,45 +96,92 @@ pub const TreeWalker = struct { return self.what_to_show; } - pub fn get_filter(self: *TreeWalker) ?Env.Callback { + pub fn get_filter(self: *TreeWalker) ?Env.Function { return self.filter; } - pub fn _firstChild(self: *TreeWalker) ?*parser.Node { - const first_child = parser.nodeFirstChild(self.current_node) catch return null; - self.current_node = first_child orelse return null; - return first_child; + pub fn _firstChild(self: *TreeWalker) !?*parser.Node { + const children = try parser.nodeGetChildNodes(self.current_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; + + if (!try self.verify_what_to_show(child)) continue; + if (!try self.verify_filter(child)) continue; + + self.depth += 1; + self.current_node = child; + return child; + } + + return null; } - pub fn _lastChild(self: *TreeWalker) ?*parser.Node { - const last_child = parser.nodeLastChild(self.current_node) catch return null; - self.current_node = last_child orelse return null; - return last_child; + pub fn _lastChild(self: *TreeWalker) !?*parser.Node { + const children = try parser.nodeGetChildNodes(self.current_node); + const child_count = try parser.nodeListLength(children); + + for (0..child_count) |i| { + const index: u32 = @intCast(child_count - 1 - i); + const child = (try parser.nodeListItem(children, index)) orelse return null; + + if (!try self.verify_what_to_show(child)) continue; + if (!try self.verify_filter(child)) continue; + + self.depth += 1; + self.current_node = child; + return child; + } + + return null; } - pub fn _nextNode(self: *TreeWalker) ?*parser.Node { + pub fn _nextNode(self: *TreeWalker) !?*parser.Node { return self._firstChild(); } - pub fn _nextSibling(self: *TreeWalker) ?*parser.Node { - const next_sibling = parser.nodeNextSibling(self.current_node) catch return null; - self.current_node = next_sibling orelse return null; - return next_sibling; + pub fn _nextSibling(self: *TreeWalker) !?*parser.Node { + var current = self.current_node; + + while (true) { + current = (try parser.nodeNextSibling(current)) orelse return null; + if (!try self.verify_what_to_show(current)) continue; + if (!try self.verify_filter(current)) continue; + break; + } + + return current; } - pub fn _parentNode(self: *TreeWalker) ?*parser.Node { - const parent = parser.nodeParentNode(self.current_node) catch return null; - self.current_node = parent orelse return null; + pub fn _parentNode(self: *TreeWalker) !?*parser.Node { + if (self.depth == 0) return null; + + const parent = (try parser.nodeParentNode(self.current_node)) orelse return null; + + if (!try self.verify_what_to_show(parent)) return null; + if (!try self.verify_filter(parent)) return null; + + self.depth -= 1; + self.current_node = parent; return parent; } - pub fn _previousNode(self: *TreeWalker) ?*parser.Node { + pub fn _previousNode(self: *TreeWalker) !?*parser.Node { return self._parentNode(); } - pub fn _previousSibling(self: *TreeWalker) ?*parser.Node { - const previous_sibling = parser.nodePreviousSibling(self.current_node) catch return null; - self.current_node = previous_sibling orelse return null; - return previous_sibling; + pub fn _previousSibling(self: *TreeWalker) !?*parser.Node { + var current = self.current_node; + + while (true) { + current = (try parser.nodePreviousSibling(current)) orelse return null; + if (!try self.verify_what_to_show(current)) continue; + if (!try self.verify_filter(current)) continue; + break; + } + + return current; } }; diff --git a/src/browser/env.zig b/src/browser/env.zig index e45ff75d..bd191448 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -42,7 +42,7 @@ const WebApis = struct { pub const JsThis = Env.JsThis; pub const JsObject = Env.JsObject; -pub const Callback = Env.Callback; +pub const Function = Env.Function; pub const Env = js.Env(*SessionState, WebApis); const Window = @import("html/window.zig").Window; diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 1d1a504d..1dfa1b5a 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -20,7 +20,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const parser = @import("../netsurf.zig"); -const Callback = @import("../env.zig").Callback; +const Function = @import("../env.zig").Function; const generate = @import("../../runtime/generate.zig"); const DOMException = @import("../dom/exceptions.zig").DOMException; @@ -140,10 +140,10 @@ pub const Event = struct { }; pub const EventHandler = struct { - callback: Callback, + callback: Function, node: parser.EventNode, - pub fn init(allocator: Allocator, callback: Callback) !*EventHandler { + pub fn init(allocator: Allocator, callback: Function) !*EventHandler { const eh = try allocator.create(EventHandler); eh.* = .{ .callback = callback, @@ -162,8 +162,8 @@ pub const EventHandler = struct { }; const self: *EventHandler = @fieldParentPtr("node", node); - var result: Callback.Result = undefined; - self.callback.tryCall(.{ievent}, &result) catch { + var result: Function.Result = undefined; + self.callback.tryCall(void, .{ievent}, &result) catch { log.err("event handler error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/html/media_query_list.zig b/src/browser/html/media_query_list.zig index b38c1b26..32ffc1d6 100644 --- a/src/browser/html/media_query_list.zig +++ b/src/browser/html/media_query_list.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const parser = @import("../netsurf.zig"); -const Callback = @import("../env.zig").Callback; +const Function = @import("../env.zig").Function; const EventTarget = @import("../dom/event_target.zig").EventTarget; // https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface @@ -39,7 +39,7 @@ pub const MediaQueryList = struct { return self.media; } - pub fn _addListener(_: *const MediaQueryList, _: Callback) void {} + pub fn _addListener(_: *const MediaQueryList, _: Function) void {} - pub fn _removeListener(_: *const MediaQueryList, _: Callback) void {} + pub fn _removeListener(_: *const MediaQueryList, _: Function) void {} }; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index e903e06c..7d747f04 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const Callback = @import("../env.zig").Callback; +const Function = @import("../env.zig").Function; const SessionState = @import("../env.zig").SessionState; const Loop = @import("../../runtime/loop.zig").Loop; @@ -159,12 +159,12 @@ pub const Window = struct { // Returns the request ID, that uniquely identifies the entry in the callback list. pub fn _requestAnimationFrame( self: *Window, - callback: Callback, + callback: Function, ) !u32 { // We immediately execute the callback, but this may not be correct TBD. // Since: When multiple callbacks queued by requestAnimationFrame() begin to fire in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback's workload. - var result: Callback.Result = undefined; - callback.tryCall(.{self.performance._now()}, &result) catch { + var result: Function.Result = undefined; + callback.tryCall(void, .{self.performance._now()}, &result) catch { log.err("Window.requestAnimationFrame(): {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; @@ -178,12 +178,12 @@ pub const Window = struct { } // TODO handle callback arguments. - pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { + pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, state: *SessionState) !u32 { return self.createTimeout(cbk, delay, state, false); } // TODO handle callback arguments. - pub fn _setInterval(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { + pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, state: *SessionState) !u32 { return self.createTimeout(cbk, delay, state, true); } @@ -204,7 +204,7 @@ pub const Window = struct { }; } - fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { + fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { if (self.timers.count() > 512) { return error.TooManyTimeout; } @@ -255,7 +255,7 @@ const TimerCallback = struct { timer_id: u31, // The JavaScript callback to execute - cbk: Callback, + cbk: Function, // This is the internal data that the event loop tracks. We'll get this // back in run and, from it, can get our TimerCallback instance @@ -269,8 +269,8 @@ const TimerCallback = struct { fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void { const self: *TimerCallback = @fieldParentPtr("node", node); - var result: Callback.Result = undefined; - self.cbk.tryCall(.{}, &result) catch { + var result: Function.Result = undefined; + self.cbk.tryCall(void, .{}, &result) catch { log.err("timeout callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/xhr/event_target.zig b/src/browser/xhr/event_target.zig index 1f11bdbc..981dd69a 100644 --- a/src/browser/xhr/event_target.zig +++ b/src/browser/xhr/event_target.zig @@ -19,7 +19,7 @@ const std = @import("std"); const Env = @import("../env.zig").Env; -const Callback = Env.Callback; +const Function = Env.Function; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventHandler = @import("../events/event.zig").EventHandler; @@ -35,18 +35,18 @@ pub const XMLHttpRequestEventTarget = struct { // Extend libdom event target for pure zig struct. base: parser.EventTargetTBase = parser.EventTargetTBase{}, - onloadstart_cbk: ?Callback = null, - onprogress_cbk: ?Callback = null, - onabort_cbk: ?Callback = null, - onload_cbk: ?Callback = null, - ontimeout_cbk: ?Callback = null, - onloadend_cbk: ?Callback = null, + onloadstart_cbk: ?Function = null, + onprogress_cbk: ?Function = null, + onabort_cbk: ?Function = null, + onload_cbk: ?Function = null, + ontimeout_cbk: ?Function = null, + onloadend_cbk: ?Function = null, fn register( self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, - cbk: Callback, + cbk: Function, ) !void { const target = @as(*parser.EventTarget, @ptrCast(self)); const eh = try EventHandler.init(alloc, try cbk.withThis(target)); @@ -69,51 +69,51 @@ pub const XMLHttpRequestEventTarget = struct { try parser.eventTargetRemoveEventListener(et, typ, lst.?, false); } - pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Function { return self.onloadstart_cbk; } - pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Function { return self.onprogress_cbk; } - pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Function { return self.onabort_cbk; } - pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Function { return self.onload_cbk; } - pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Function { return self.ontimeout_cbk; } - pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback { + pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function { return self.onloadend_cbk; } - pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id); try self.register(state.arena, "loadstart", handler); self.onloadstart_cbk = handler; } - pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id); try self.register(state.arena, "progress", handler); self.onprogress_cbk = handler; } - pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id); try self.register(state.arena, "abort", handler); self.onabort_cbk = handler; } - pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id); try self.register(state.arena, "load", handler); self.onload_cbk = handler; } - pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id); try self.register(state.arena, "timeout", handler); self.ontimeout_cbk = handler; } - pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id); try self.register(state.arena, "loadend", handler); self.onloadend_cbk = handler; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 51960b10..1e1e6c6f 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -947,7 +947,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Extracted so that it can be used in both jsValueToZig and in // probeJsValueToZig. Avoids having to duplicate this logic when probing. fn jsValueToStruct(self: *Scope, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T { - if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) { + if (@hasDecl(T, "_FUNCTION_ID_KLUDGE")) { if (!js_value.isFunction()) { return error.InvalidArgument; } @@ -1218,25 +1218,25 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } }; - pub const Callback = struct { + pub const Function = struct { id: usize, scope: *Scope, this: ?v8.Object = null, func: PersistentFunction, // We use this when mapping a JS value to a Zig object. We can't - // Say we have a Zig function that takes a Callback, we can't just - // check param.type == Callback, because Callback is a generic. + // Say we have a Zig function that takes a Function, we can't just + // check param.type == Function, because Function is a generic. // So, as a quick hack, we can determine if the Zig type is a - // callback by checking @hasDecl(T, "_CALLBACK_ID_KLUDGE") - const _CALLBACK_ID_KLUDGE = true; + // callback by checking @hasDecl(T, "_FUNCTION_ID_KLUDGE") + const _FUNCTION_ID_KLUDGE = true; pub const Result = struct { stack: ?[]const u8, exception: []const u8, }; - pub fn withThis(self: *const Callback, value: anytype) !Callback { + pub fn withThis(self: *const Function, value: anytype) !Function { return .{ .id = self.id, .func = self.func, @@ -1245,20 +1245,20 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } - pub fn call(self: *const Callback, args: anytype) !void { - return self.callWithThis(self.getThis(), args); + pub fn call(self: *const Function, comptime T: type, args: anytype) !T { + return self.callWithThis(T, self.getThis(), args); } - pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void { - return self.tryCallWithThis(self.getThis(), args, result); + pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T { + return self.tryCallWithThis(T, self.getThis(), args, result); } - pub fn tryCallWithThis(self: *const Callback, this: anytype, args: anytype, result: *Result) !void { + pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !void { var try_catch: TryCatch = undefined; try_catch.init(self.scope); defer try_catch.deinit(); - self.callWithThis(this, args) catch |err| { + return self.callWithThis(T, this, args) catch |err| { if (try_catch.hasCaught()) { const allocator = self.scope.call_arena; result.stack = try_catch.stack(allocator) catch null; @@ -1271,7 +1271,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } - pub fn callWithThis(self: *const Callback, this: anytype, args: anytype) !void { + pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { const scope = self.scope; const js_this = try scope.valueToExistingObject(this); @@ -1287,14 +1287,18 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { if (result == null) { return error.JSExecCallback; } + + if (@typeInfo(T) == .void) return {}; + const named_function = comptime NamedFunction.init(T, "callResult"); + return scope.jsValueToZig(named_function, T, result.?); } - fn getThis(self: *const Callback) v8.Object { + fn getThis(self: *const Function) v8.Object { return self.this orelse self.scope.context.getGlobal(); } // debug/helper to print the source of the JS callback - pub fn printFunc(self: Callback) !void { + pub fn printFunc(self: Function) !void { const scope = self.scope; const value = self.func.castToFunction().toValue(); const src = try valueToString(scope.call_arena, value, scope.isolate, scope.context); @@ -1453,7 +1457,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Env.JsObject. Want a TypedArray? Env.TypedArray. pub fn TypedArray(comptime T: type) type { return struct { - // See Callback._CALLBACK_ID_KLUDGE + // See Function._FUNCTION_ID_KLUDGE const _TYPED_ARRAY_ID_KLUDGE = true; values: []const T, @@ -1987,7 +1991,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return js_obj.toValue(); } - if (T == Callback) { + if (T == Function) { // we're returnig a callback return value.func.toValue(); } From 38c6fa9c7610d081e5c9b393286eed2d8a51c8a9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 22 May 2025 13:02:08 +0800 Subject: [PATCH 4/5] Don't error when failing to convert type to function. Because jsValueToStruct is now used in union probing, it shouldn't fail on a mismatch, but rather return null. It's up to the caller to decide whether that's an error or not. --- src/runtime/js.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 1e1e6c6f..2d763728 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -910,7 +910,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // compatible with. A compatible field has higher precedence // than a coercible, but still isn't a perfect match. var compatible_index: ?usize = null; - inline for (u.fields, 0..) |field, i| { switch (try self.probeJsValueToZig(named_function, field.type, js_value)) { .value => |v| return @unionInit(T, field.name, v), @@ -949,7 +948,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { fn jsValueToStruct(self: *Scope, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T { if (@hasDecl(T, "_FUNCTION_ID_KLUDGE")) { if (!js_value.isFunction()) { - return error.InvalidArgument; + return null; } const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function)); From 97d414aa00edbec92af591c75946d649c7fa383b Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 22 May 2025 09:44:06 -0700 Subject: [PATCH 5/5] Fixing TreeWalker Filtering --- src/browser/dom/tree_walker.zig | 214 ++++++++++++++++++++++++-------- 1 file changed, 160 insertions(+), 54 deletions(-) diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig index 792c0a2a..826bb062 100644 --- a/src/browser/dom/tree_walker.zig +++ b/src/browser/dom/tree_walker.zig @@ -21,6 +21,7 @@ const parser = @import("../netsurf.zig"); const NodeFilter = @import("node_filter.zig").NodeFilter; const Env = @import("../env.zig").Env; +const SessionState = @import("../env.zig").SessionState; // https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker pub const TreeWalker = struct { @@ -29,8 +30,6 @@ pub const TreeWalker = struct { what_to_show: u32, filter: ?Env.Function, - depth: usize, - pub const TreeWalkerOpts = union(enum) { function: Env.Function, object: struct { acceptNode: Env.Function }, @@ -51,14 +50,17 @@ pub const TreeWalker = struct { .current_node = node, .what_to_show = what_to_show orelse NodeFilter._SHOW_ALL, .filter = filter_func, - .depth = 0, }; } - fn verify_what_to_show(self: *const TreeWalker, node: *parser.Node) !bool { + const VerifyResult = enum { accept, skip, reject }; + + fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult { const node_type = try parser.nodeType(node); const what_to_show = self.what_to_show; - return switch (node_type) { + + // Verify that we can show this node type. + if (!switch (node_type) { .attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0, .cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0, .comment => what_to_show & NodeFilter._SHOW_COMMENT != 0, @@ -71,17 +73,18 @@ pub const TreeWalker = struct { .notation => what_to_show & NodeFilter._SHOW_NOTATION != 0, .processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0, .text => what_to_show & NodeFilter._SHOW_TEXT != 0, - }; - } + }) return .reject; - fn verify_filter(self: *const TreeWalker, node: *parser.Node) !bool { + // Verify that we aren't filtering it out. if (self.filter) |f| { const filter = try f.call(u32, .{node}); return switch (filter) { - NodeFilter._FILTER_ACCEPT => true, - else => false, + NodeFilter._FILTER_ACCEPT => .accept, + NodeFilter._FILTER_REJECT => .reject, + NodeFilter._FILTER_SKIP => .skip, + else => .reject, }; - } else return true; + } else return .accept; } pub fn get_root(self: *TreeWalker) *parser.Node { @@ -100,18 +103,94 @@ pub const TreeWalker = struct { return self.filter; } - pub fn _firstChild(self: *TreeWalker) !?*parser.Node { - const children = try parser.nodeGetChildNodes(self.current_node); + pub fn set_currentNode(self: *TreeWalker, node: *parser.Node) !void { + self.current_node = node; + } + + fn firstChild(self: *const TreeWalker, 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; - if (!try self.verify_what_to_show(child)) continue; - if (!try self.verify_filter(child)) continue; + switch (try self.verify(child)) { + .accept => return child, + .reject => continue, + .skip => if (try self.firstChild(child)) |gchild| return gchild, + } + } - self.depth += 1; + return null; + } + + fn lastChild(self: *const TreeWalker, 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.verify(child)) { + .accept => return child, + .reject => continue, + .skip => if (try self.lastChild(child)) |gchild| return gchild, + } + } + + return null; + } + + fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { + var current = node; + + while (true) { + current = (try parser.nodeNextSibling(current)) orelse return null; + + switch (try self.verify(current)) { + .accept => return current, + .skip, .reject => continue, + } + } + + return null; + } + + fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { + var current = node; + + while (true) { + current = (try parser.nodePreviousSibling(current)) orelse return null; + + switch (try self.verify(current)) { + .accept => return current, + .skip, .reject => continue, + } + } + + return null; + } + + fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { + if (self.root == node) return null; + + var current = node; + while (true) { + if (current == self.root) return null; + current = (try parser.nodeParentNode(current)) orelse return null; + + switch (try self.verify(current)) { + .accept => return current, + .reject, .skip => continue, + } + } + } + + pub fn _firstChild(self: *TreeWalker) !?*parser.Node { + if (try self.firstChild(self.current_node)) |child| { self.current_node = child; return child; } @@ -120,17 +199,7 @@ pub const TreeWalker = struct { } pub fn _lastChild(self: *TreeWalker) !?*parser.Node { - const children = try parser.nodeGetChildNodes(self.current_node); - const child_count = try parser.nodeListLength(children); - - for (0..child_count) |i| { - const index: u32 = @intCast(child_count - 1 - i); - const child = (try parser.nodeListItem(children, index)) orelse return null; - - if (!try self.verify_what_to_show(child)) continue; - if (!try self.verify_filter(child)) continue; - - self.depth += 1; + if (try self.lastChild(self.current_node)) |child| { self.current_node = child; return child; } @@ -139,49 +208,86 @@ pub const TreeWalker = struct { } pub fn _nextNode(self: *TreeWalker) !?*parser.Node { - return self._firstChild(); + if (try self.firstChild(self.current_node)) |child| { + self.current_node = child; + return child; + } + + var current = self.current_node; + while (current != self.root) { + if (try self.nextSibling(current)) |sibling| { + self.current_node = sibling; + return sibling; + } + + current = (try parser.nodeParentNode(current)) orelse break; + } + + return null; } pub fn _nextSibling(self: *TreeWalker) !?*parser.Node { - var current = self.current_node; - - while (true) { - current = (try parser.nodeNextSibling(current)) orelse return null; - if (!try self.verify_what_to_show(current)) continue; - if (!try self.verify_filter(current)) continue; - break; + if (try self.nextSibling(self.current_node)) |sibling| { + self.current_node = sibling; + return sibling; } - return current; + return null; } pub fn _parentNode(self: *TreeWalker) !?*parser.Node { - if (self.depth == 0) return null; + if (try self.parentNode(self.current_node)) |parent| { + self.current_node = parent; + return parent; + } - const parent = (try parser.nodeParentNode(self.current_node)) orelse return null; - - if (!try self.verify_what_to_show(parent)) return null; - if (!try self.verify_filter(parent)) return null; - - self.depth -= 1; - self.current_node = parent; - return parent; + return null; } pub fn _previousNode(self: *TreeWalker) !?*parser.Node { - return self._parentNode(); + var current = self.current_node; + while (try parser.nodePreviousSibling(current)) |previous| { + current = previous; + + switch (try self.verify(current)) { + .accept => { + // Get last child if it has one. + if (try self.lastChild(current)) |child| { + self.current_node = child; + return child; + } + + // Otherwise, this node is our previous one. + self.current_node = current; + return current; + }, + .reject => continue, + .skip => { + // Get last child if it has one. + if (try self.lastChild(current)) |child| { + self.current_node = child; + return child; + } + }, + } + } + + if (current != self.root) { + if (try self.parentNode(current)) |parent| { + self.current_node = parent; + return parent; + } + } + + return null; } pub fn _previousSibling(self: *TreeWalker) !?*parser.Node { - var current = self.current_node; - - while (true) { - current = (try parser.nodePreviousSibling(current)) orelse return null; - if (!try self.verify_what_to_show(current)) continue; - if (!try self.verify_filter(current)) continue; - break; + if (try self.previousSibling(self.current_node)) |sibling| { + self.current_node = sibling; + return sibling; } - return current; + return null; } };