Rework HTMLAllCollection

Capture its unique properties:
1- instances are falsy, and
2- instance can be called as a function

The behavior is used for browser detection (i.e. duckduckgo treats us as a
legacy browser because we document.all != false)
This commit is contained in:
Karl Seguin
2025-05-15 22:39:03 +08:00
parent 210d4f6aa1
commit b08ffcc437
7 changed files with 78 additions and 16 deletions

View File

@@ -17,7 +17,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.1.22' default: 'v0.1.23'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false

View File

@@ -5,7 +5,7 @@ ARG ZIG=0.14.0
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG ARCH=x86_64 ARG ARCH=x86_64
ARG V8=11.1.134 ARG V8=11.1.134
ARG ZIG_V8=v0.1.22 ARG ZIG_V8=v0.1.23
RUN apt-get update -yq && \ RUN apt-get update -yq && \
apt-get install -yq xz-utils \ apt-get install -yq xz-utils \

View File

@@ -13,8 +13,8 @@
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
}, },
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/4809111f930293c6d5082971ad7ffc3d822b6f37.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/6f1ee74a0e7002ea3568e337ab716c1e75c53769.tar.gz",
.hash = "v8-0.0.0-xddH632xAwAjF7ieh48tjbMpu7fVVGr3r3aLwmBbFvPk", .hash = "v8-0.0.0-xddH6z2yAwCOPUGmy1IgXysI1yWt8ftd2Z3D5zp0I9tV",
}, },
//.v8 = .{ .path = "../zig-v8-fork" }, //.v8 = .{ .path = "../zig-v8-fork" },
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" }, //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },

View File

@@ -149,17 +149,43 @@ pub fn HTMLCollectionByName(
}; };
} }
pub fn HTMLCollectionAll( // HTMLAllCollection is a special type: instances of it are falsy. It's the only
root: ?*parser.Node, // object in the WebAPI that behaves like this - in fact, it's even a special
include_root: bool, // case in the JavaScript spec.
) !HTMLCollection { // This is important, because a lot of browser detection rely on this behavior
return HTMLCollection{ // to determine what browser is running.
// It's also possible to use an instance like a function:
// document.all(3)
// document.all('some_id')
pub const HTMLAllCollection = struct {
pub const prototype = *HTMLCollection;
proto: HTMLCollection,
pub const mark_as_undetectable = true;
pub fn init(root: ?*parser.Node) HTMLAllCollection {
return .{ .proto = .{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchTrue = .{} }, .matcher = .{ .matchTrue = .{} },
.include_root = include_root, .include_root = true,
} };
}
const CAllAsFunctionArg = union(enum) {
index: u32,
id: []const u8,
};
pub fn jsCallAsFunction(self: *HTMLAllCollection, arg: CAllAsFunctionArg) !?Union {
return switch (arg) {
.index => |i| self.proto._item(i),
.id => |id| self.proto._namedItem(id),
}; };
} }
};
pub fn HTMLCollectionChildren( pub fn HTMLCollectionChildren(
root: ?*parser.Node, root: ?*parser.Node,

View File

@@ -33,6 +33,7 @@ const Document = @import("document.zig").Document;
const DocumentType = @import("document_type.zig").DocumentType; const DocumentType = @import("document_type.zig").DocumentType;
const DocumentFragment = @import("document_fragment.zig").DocumentFragment; const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const HTMLCollection = @import("html_collection.zig").HTMLCollection; const HTMLCollection = @import("html_collection.zig").HTMLCollection;
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator; const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
const Walker = @import("walker.zig").WalkerDepthFirst; const Walker = @import("walker.zig").WalkerDepthFirst;
@@ -50,6 +51,7 @@ pub const Interfaces = .{
DocumentType, DocumentType,
DocumentFragment, DocumentFragment,
HTMLCollection, HTMLCollection,
HTMLAllCollection,
HTMLCollectionIterator, HTMLCollectionIterator,
HTML.Interfaces, HTML.Interfaces,
}; };

View File

@@ -151,8 +151,8 @@ pub const HTMLDocument = struct {
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false); return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
} }
pub fn get_all(self: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
return try collection.HTMLCollectionAll(parser.documentHTMLToNode(self), true); return collection.HTMLAllCollection.init(parser.documentHTMLToNode(self));
} }
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script { pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
@@ -260,4 +260,11 @@ test "Browser.HTML.Document" {
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" }, .{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, .{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{}); }, .{});
try runner.testCases(&.{
.{ "!document.all", "true" },
.{ "!!document.all", "false" },
.{ "document.all(5)", "[object HTMLParagraphElement]" },
.{ "document.all('content')", "[object HTMLDivElement]" },
}, .{});
} }

View File

@@ -1213,6 +1213,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
generateIndexer(Struct, template_proto); generateIndexer(Struct, template_proto);
generateNamedIndexer(Struct, template_proto); generateNamedIndexer(Struct, template_proto);
generateUndetectable(Struct, template.getInstanceTemplate());
} }
// Even if a struct doesn't have a `constructor` function, we still // Even if a struct doesn't have a `constructor` function, we still
@@ -1412,6 +1413,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
template_proto.setNamedProperty(configuration, null); template_proto.setNamedProperty(configuration, null);
} }
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(Self, State).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 (!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.");
}
template.markAsUndetectable();
}
}
// Turns a Zig value into a JS one. // Turns a Zig value into a JS one.
fn zigValueToJs( fn zigValueToJs(
templates: []v8.FunctionTemplate, templates: []v8.FunctionTemplate,