diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 69ed6a91..58ff9b11 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -17,7 +17,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.1.22' + default: 'v0.1.23' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 6944a011..70d609d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ARG ZIG=0.14.0 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ARCH=x86_64 ARG V8=11.1.134 -ARG ZIG_V8=v0.1.22 +ARG ZIG_V8=v0.1.23 RUN apt-get update -yq && \ apt-get install -yq xz-utils \ diff --git a/build.zig.zon b/build.zig.zon index 1e8805b5..58f5508f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", }, .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/4809111f930293c6d5082971ad7ffc3d822b6f37.tar.gz", - .hash = "v8-0.0.0-xddH632xAwAjF7ieh48tjbMpu7fVVGr3r3aLwmBbFvPk", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/6f1ee74a0e7002ea3568e337ab716c1e75c53769.tar.gz", + .hash = "v8-0.0.0-xddH6z2yAwCOPUGmy1IgXysI1yWt8ftd2Z3D5zp0I9tV", }, //.v8 = .{ .path = "../zig-v8-fork" }, //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" }, diff --git a/src/browser/dom/html_collection.zig b/src/browser/dom/html_collection.zig index 1291a0aa..7c9286ac 100644 --- a/src/browser/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -149,17 +149,43 @@ pub fn HTMLCollectionByName( }; } -pub fn HTMLCollectionAll( - root: ?*parser.Node, - include_root: bool, -) !HTMLCollection { - return HTMLCollection{ - .root = root, - .walker = .{ .walkerDepthFirst = .{} }, - .matcher = .{ .matchTrue = .{} }, - .include_root = include_root, +// HTMLAllCollection is a special type: instances of it are falsy. It's the only +// object in the WebAPI that behaves like this - in fact, it's even a special +// case in the JavaScript spec. +// This is important, because a lot of browser detection rely on this behavior +// 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, + .walker = .{ .walkerDepthFirst = .{} }, + .matcher = .{ .matchTrue = .{} }, + .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( root: ?*parser.Node, diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 79e89cf2..7f2747e1 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -33,6 +33,7 @@ const Document = @import("document.zig").Document; const DocumentType = @import("document_type.zig").DocumentType; const DocumentFragment = @import("document_fragment.zig").DocumentFragment; const HTMLCollection = @import("html_collection.zig").HTMLCollection; +const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection; const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator; const Walker = @import("walker.zig").WalkerDepthFirst; @@ -50,6 +51,7 @@ pub const Interfaces = .{ DocumentType, DocumentFragment, HTMLCollection, + HTMLAllCollection, HTMLCollectionIterator, HTML.Interfaces, }; diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 95619940..89370f50 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -151,8 +151,8 @@ pub const HTMLDocument = struct { return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false); } - pub fn get_all(self: *parser.DocumentHTML) !collection.HTMLCollection { - return try collection.HTMLCollectionAll(parser.documentHTMLToNode(self), true); + pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection { + return collection.HTMLAllCollection.init(parser.documentHTMLToNode(self)); } 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", "name=Oeschger; favorite_food=tripe" }, }, .{}); + + try runner.testCases(&.{ + .{ "!document.all", "true" }, + .{ "!!document.all", "false" }, + .{ "document.all(5)", "[object HTMLParagraphElement]" }, + .{ "document.all('content')", "[object HTMLDivElement]" }, + }, .{}); } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 6d7cd275..d3d68873 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1221,6 +1221,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { generateIndexer(Struct, template_proto); generateNamedIndexer(Struct, template_proto); + generateUndetectable(Struct, template.getInstanceTemplate()); } // Even if a struct doesn't have a `constructor` function, we still @@ -1420,6 +1421,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { 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. fn zigValueToJs( templates: []v8.FunctionTemplate,