mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-30 07:31:47 +00:00
HTMLAllCollection
This commit is contained in:
@@ -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();
|
||||
// }
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
68
src/browser/tests/document/all_collection.html
Normal file
68
src/browser/tests/document/all_collection.html
Normal 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>
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
169
src/browser/webapi/collections/HTMLAllCollection.zig
Normal file
169
src/browser/webapi/collections/HTMLAllCollection.zig
Normal 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 });
|
||||
};
|
||||
Reference in New Issue
Block a user