diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 726f5fbf..ad5f125e 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -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(); // } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 6928a495..1186d54f 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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)) { diff --git a/src/browser/tests/document/all_collection.html b/src/browser/tests/document/all_collection.html new file mode 100644 index 00000000..380b4088 --- /dev/null +++ b/src/browser/tests/document/all_collection.html @@ -0,0 +1,68 @@ + + + + Test Page + + +
First
+ Second +

Third

+ Link + + + + diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 618d2ac5..9177fa7b 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -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, .{}); }; diff --git a/src/browser/webapi/collections.zig b/src/browser/webapi/collections.zig index f3d1caab..cb6b2daa 100644 --- a/src/browser/webapi/collections.zig +++ b/src/browser/webapi/collections.zig @@ -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, }; diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig new file mode 100644 index 00000000..27429fd4 --- /dev/null +++ b/src/browser/webapi/collections/HTMLAllCollection.zig @@ -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 }); +};