Merge pull request #1718 from lightpanda-io/enhance-treewalker

Enhance TreeWalker
This commit is contained in:
Karl Seguin
2026-03-05 15:28:04 +08:00
committed by GitHub
2 changed files with 140 additions and 39 deletions

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const URL = @import("URL.zig"); const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const CData = @import("webapi/CData.zig"); const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
@@ -114,26 +115,24 @@ fn isAllWhitespace(text: []const u8) bool {
} else true; } else true;
} }
fn hasBlockDescendant(node: *Node) bool { fn hasBlockDescendant(root: *Node) bool {
var it = node.childrenIterator(); var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{});
return while (it.next()) |child| { while (tw.next()) |el| {
if (child.is(Element)) |el| { if (isBlock(el.getTag())) return true;
if (isBlock(el.getTag())) break true; }
if (hasBlockDescendant(child)) break true; return false;
}
} else false;
} }
fn hasVisibleContent(node: *Node) bool { fn hasVisibleContent(root: *Node) bool {
var it = node.childrenIterator(); var tw = TreeWalker.FullExcludeSelf.init(root, .{});
while (it.next()) |child| { while (tw.next()) |node| {
if (isSignificantText(child)) return true; if (isSignificantText(node)) return true;
if (child.is(Element)) |el| { if (node.is(Element)) |el| {
if (!isVisibleElement(el)) continue; if (!isVisibleElement(el)) {
// Images are visible tw.skipChildren();
if (el.getTag() == .img) return true; } else if (el.getTag() == .img) {
// Recursive check return true;
if (hasVisibleContent(child)) return true; }
} }
} }
return false; return false;

View File

@@ -31,6 +31,7 @@ const Mode = enum {
pub fn TreeWalker(comptime mode: Mode) type { pub fn TreeWalker(comptime mode: Mode) type {
return struct { return struct {
_current: ?*Node = null,
_next: ?*Node, _next: ?*Node,
_root: *Node, _root: *Node,
@@ -47,37 +48,74 @@ pub fn TreeWalker(comptime mode: Mode) type {
pub fn next(self: *Self) ?*Node { pub fn next(self: *Self) ?*Node {
const node = self._next orelse return null; const node = self._next orelse return null;
self._current = node;
if (comptime mode == .children) { if (comptime mode == .children) {
self._next = Node.linkToNodeOrNull(node._child_link.next); self._next = Node.linkToNodeOrNull(node._child_link.next);
return node; return node;
} }
if (node._children) |children| { self._next = self.computeNextInDocumentOrder(node);
self._next = children.first();
} else if (node._child_link.next) |n| {
self._next = Node.linkToNode(n);
} else {
// No children, no next sibling - walk up until we find a next sibling or hit root
var current = node._parent;
while (current) |parent| {
if (parent == self._root) {
self._next = null;
break;
}
if (parent._child_link.next) |next_sibling| {
self._next = Node.linkToNode(next_sibling);
break;
}
current = parent._parent;
} else {
self._next = null;
}
}
return node; return node;
} }
pub fn skipChildren(self: *Self) void {
if (comptime mode == .children) return;
const current = self._current orelse return;
self._next = self.computeNextSiblingOrUncle(current);
}
pub fn nextSibling(self: *Self) ?*Node {
const current = self._current orelse return null;
const sibling = Node.linkToNodeOrNull(current._child_link.next) orelse return null;
self._current = sibling;
if (comptime mode == .children) {
self._next = Node.linkToNodeOrNull(sibling._child_link.next);
} else {
self._next = self.computeNextInDocumentOrder(sibling);
}
return sibling;
}
pub fn previousSibling(self: *Self) ?*Node {
const current = self._current orelse return null;
const sibling = Node.linkToNodeOrNull(current._child_link.prev) orelse return null;
self._current = sibling;
if (comptime mode == .children) {
self._next = Node.linkToNodeOrNull(sibling._child_link.next);
} else {
self._next = self.computeNextInDocumentOrder(sibling);
}
return sibling;
}
fn computeNextInDocumentOrder(self: *Self, node: *Node) ?*Node {
if (node._children) |children| {
return children.first();
}
return self.computeNextSiblingOrUncle(node);
}
fn computeNextSiblingOrUncle(self: *Self, node: *Node) ?*Node {
if (node._child_link.next) |n| {
return Node.linkToNode(n);
}
var current = node._parent;
while (current) |parent| {
if (parent == self._root) return null;
if (parent._child_link.next) |next_sibling| {
return Node.linkToNode(next_sibling);
}
current = parent._parent;
}
return null;
}
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self._current = null;
self._next = firstNext(self._root); self._next = firstNext(self._root);
} }
@@ -147,3 +185,67 @@ pub fn TreeWalker(comptime mode: Mode) type {
}; };
}; };
} }
test "TreeWalker: skipChildren" {
const testing = @import("../../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const doc = page.window._document;
// <div>
// <span>
// <b>A</b>
// </span>
// <p>B</p>
// </div>
const div = try doc.createElement("div", null, page);
const span = try doc.createElement("span", null, page);
const b = try doc.createElement("b", null, page);
const p = try doc.createElement("p", null, page);
_ = try span.asNode().appendChild(b.asNode(), page);
_ = try div.asNode().appendChild(span.asNode(), page);
_ = try div.asNode().appendChild(p.asNode(), page);
var tw = Full.init(div.asNode(), .{});
// root (div)
try testing.expect(tw.next() == div.asNode());
// span
try testing.expect(tw.next() == span.asNode());
// skip children of span (should jump over <b> to <p>)
tw.skipChildren();
try testing.expect(tw.next() == p.asNode());
try testing.expect(tw.next() == null);
}
test "TreeWalker: sibling navigation" {
const testing = @import("../../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const doc = page.window._document;
// <div>
// <span>A</span>
// <p>B</p>
// </div>
const div = try doc.createElement("div", null, page);
const span = try doc.createElement("span", null, page);
const p = try doc.createElement("p", null, page);
_ = try div.asNode().appendChild(span.asNode(), page);
_ = try div.asNode().appendChild(p.asNode(), page);
var tw = Full.init(div.asNode(), .{});
// Move to span
_ = tw.next(); // div
_ = tw.next(); // span
// nextSibling -> p
try testing.expect(tw.nextSibling() == p.asNode());
// previousSibling -> span
try testing.expect(tw.previousSibling() == span.asNode());
}