HTMLAllCollection

This commit is contained in:
Karl Seguin
2025-10-30 11:30:06 +08:00
parent 5ae1190ddd
commit c966211481
6 changed files with 284 additions and 24 deletions

View File

@@ -279,6 +279,12 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct
else => {},
}
}
if (@hasDecl(JsApi.Meta, "htmldda")) {
const instance_template = template.getInstanceTemplate();
instance_template.markAsUndetectable();
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
}
}
// Even if a struct doesn't have a `constructor` function, we still
@@ -315,28 +321,15 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
return template;
}
// ZIGDOM (HTMLAllCollection I think)
// fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
// const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
// if (has_js_call_as_function) {
// template.setCallAsFunctionHandler(struct {
// fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
// const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
// var caller = Caller.init(info);
// defer caller.deinit();
// const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
// caller.method(Struct, named_function, info) catch |err| {
// caller.handleError(Struct, named_function, err, info);
// };
// }
// }.callback);
// }
// if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
// if (@hasDecl(Struct, "htmldda") and Struct.htmldda) {
// if (!has_js_call_as_function) {
// @compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
// @compileError(@typeName(Struct) ++ ": htmldda required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
// }
// template.markAsUndetectable();
// }

View File

@@ -52,6 +52,10 @@ pub fn Builder(comptime T: type) type {
return Iterator.init(T, func, opts);
}
pub fn callable(comptime func: anytype, comptime opts: Callable.Opts) Callable {
return Callable.init(T, func, opts);
}
pub fn property(value: anytype) Property.GetType(@TypeOf(value)) {
return Property.GetType(@TypeOf(value)).init(value);
}
@@ -264,6 +268,28 @@ pub const Iterator = struct {
}
};
pub const Callable = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
return .{.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined,
});
}}.wrap
};
}
};
pub const Property = struct {
fn GetType(comptime T: type) type {
switch (@typeInfo(T)) {

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<div id="first">First</div>
<span name="second">Second</span>
<p id="third">Third</p>
<a href="#" name="link">Link</a>
<script src="../testing.js"></script>
<script id=all_collection>
testing.expectEqual('undefined', typeof document.all);
testing.expectEqual('HTMLAllCollection', document.all.constructor.name);
testing.expectEqual(true, document.all == null);
testing.expectEqual(true, document.all == undefined);
testing.expectEqual(false, document.all === undefined);
testing.expectEqual(true, !document.all);
testing.expectEqual(false, !!document.all);
testing.expectEqual(10, document.all.length);
testing.expectEqual('HTML', document.all[0].tagName);
testing.expectEqual('HEAD', document.all[1].tagName);
testing.expectEqual('TITLE', document.all[2].tagName);
testing.expectEqual('BODY', document.all[3].tagName);
testing.expectEqual('DIV', document.all[4].tagName);
testing.expectEqual('DIV', document.all.first.tagName);
testing.expectEqual('First', document.all.first.textContent);
testing.expectEqual('P', document.all.third.tagName);
testing.expectEqual('Third', document.all.third.textContent);
testing.expectEqual('SPAN', document.all.second.tagName);
testing.expectEqual('Second', document.all.second.textContent);
testing.expectEqual('A', document.all.link.tagName);
testing.expectEqual('SPAN', document.all.item(5).tagName);
testing.expectEqual(null, document.all.item(999));
testing.expectEqual(null, document.all.item(-1));
testing.expectEqual('DIV', document.all.namedItem('first').tagName);
testing.expectEqual('SPAN', document.all.namedItem('second').tagName);
testing.expectEqual(null, document.all.namedItem('nonexistent'));
// Test callable functionality: document.all(index) and document.all(name)
testing.expectEqual('HTML', document.all(0).tagName);
testing.expectEqual('HEAD', document.all(1).tagName);
testing.expectEqual('DIV', document.all(4).tagName);
testing.expectEqual('DIV', document.all('first').tagName);
testing.expectEqual('First', document.all('first').textContent);
testing.expectEqual('SPAN', document.all('second').tagName);
testing.expectEqual('Second', document.all('second').textContent);
testing.expectEqual('P', document.all('third').tagName);
testing.expectEqual(undefined, document.all(999));
testing.expectEqual(undefined, document.all('nonexistent'));
let count = 0;
for (const el of document.all) {
count++;
}
testing.expectEqual(10, count);
const plainDoc = new Document();
testing.expectEqual('undefined', typeof plainDoc.all);
</script>
</body>
</html>

View File

@@ -5,6 +5,7 @@ const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const Document = @import("Document.zig");
const Element = @import("Element.zig");
const collections = @import("collections.zig");
const HTMLDocument = @This();
@@ -74,23 +75,19 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
}
}
pub fn getImages(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
pub fn getImages(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .img, page);
}
pub fn getScripts(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
pub fn getScripts(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .script, page);
}
pub fn getLinks(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .anchor, page);
}
pub fn getForms(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
pub fn getForms(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .form, page);
}
@@ -102,6 +99,10 @@ pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
return self._proto._location;
}
pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {
return page._factory.create(collections.HTMLAllCollection.init(self.asNode(), page));
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLDocument);
@@ -118,7 +119,6 @@ pub const JsApi = struct {
});
}
// HTML-specific properties
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
pub const body = bridge.accessor(HTMLDocument.getBody, null, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});
@@ -128,4 +128,5 @@ pub const JsApi = struct {
pub const forms = bridge.accessor(HTMLDocument.getForms, null, .{});
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
pub const location = bridge.accessor(HTMLDocument.getLocation, null, .{ .cache = "location" });
pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});
};

View File

@@ -1,6 +1,7 @@
pub const NodeLive = @import("collections/node_live.zig").NodeLive;
pub const ChildNodes = @import("collections/ChildNodes.zig");
pub const DOMTokenList = @import("collections/DOMTokenList.zig");
pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig");
pub fn registerTypes() []const type {
return &.{
@@ -10,6 +11,8 @@ pub fn registerTypes() []const type {
@import("collections/NodeList.zig").KeyIterator,
@import("collections/NodeList.zig").ValueIterator,
@import("collections/NodeList.zig").EntryIterator,
@import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator,
DOMTokenList,
DOMTokenList.Iterator,
};

View File

@@ -0,0 +1,169 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const HTMLAllCollection = @This();
_tw: TreeWalker.FullExcludeSelf,
_last_index: usize,
_last_length: ?u32,
_cached_version: usize,
pub fn init(root: *Node, page: *Page) HTMLAllCollection {
return .{
._last_index = 0,
._last_length = null,
._tw = TreeWalker.FullExcludeSelf.init(root, .{}),
._cached_version = page.version,
};
}
fn versionCheck(self: *HTMLAllCollection, page: *const Page) bool {
if (self._cached_version != page.version) {
self._cached_version = page.version;
self._last_index = 0;
self._last_length = null;
self._tw.reset();
return false;
}
return true;
}
pub fn length(self: *HTMLAllCollection, page: *const Page) u32 {
if (self.versionCheck(page)) {
if (self._last_length) |cached_length| {
return cached_length;
}
}
std.debug.assert(self._last_index == 0);
var tw = &self._tw;
defer tw.reset();
var l: u32 = 0;
while (tw.next()) |node| {
if (node.is(Element) != null) {
l += 1;
}
}
self._last_length = l;
return l;
}
pub fn getAtIndex(self: *HTMLAllCollection, index: usize, page: *const Page) ?*Element {
_ = self.versionCheck(page);
var current = self._last_index;
if (index <= current) {
current = 0;
self._tw.reset();
}
defer self._last_index = current + 1;
const tw = &self._tw;
while (tw.next()) |node| {
if (node.is(Element)) |el| {
if (index == current) {
return el;
}
current += 1;
}
}
return null;
}
pub fn getByName(self: *HTMLAllCollection, name: []const u8, page: *Page) ?*Element {
// First, try fast ID lookup using the document's element map
if (page.document._elements_by_id.get(name)) |el| {
return el;
}
// Fall back to searching by name attribute
// Clone the tree walker to preserve _last_index optimization
_ = self.versionCheck(page);
var tw = self._tw.clone();
tw.reset();
while (tw.next()) |node| {
if (node.is(Element)) |el| {
if (el.getAttributeSafe("name")) |attr_name| {
if (std.mem.eql(u8, attr_name, name)) {
return el;
}
}
}
}
return null;
}
const CAllAsFunctionArg = union(enum) {
index: u32,
id: []const u8,
};
pub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, page: *Page) ?*Element {
return switch (arg) {
.index => |i| self.getAtIndex(i, page),
.id => |id| self.getByName(id, page),
};
}
pub fn iterator(self: *HTMLAllCollection, page: *Page) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = self._tw.clone(),
}, page);
}
const GenericIterator = @import("iterator.zig").Entry;
pub const Iterator = GenericIterator(struct {
list: *HTMLAllCollection,
tw: TreeWalker.FullExcludeSelf,
pub fn next(self: *@This(), _: *Page) ?*Element {
while (self.tw.next()) |node| {
if (node.is(Element)) |el| {
return el;
}
}
return null;
}
}, null);
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLAllCollection);
pub const Meta = struct {
pub const name = "HTMLAllCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_index: u16 = 0;
// This is a very weird class that requires special JavaScript behavior
// this htmldda and callable are only used here..
pub const htmldda = true;
pub const callable = JsApi.callable;
};
pub const length = bridge.accessor(HTMLAllCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, .{ .null_as_undefined = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element {
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
}
pub const namedItem = bridge.function(HTMLAllCollection.getByName, .{});
pub const symbol_iterator = bridge.iterator(HTMLAllCollection.iterator, .{});
pub const callable = bridge.callable(HTMLAllCollection.callable, .{ .null_as_undefined = true });
};