From 1f45d5b8e4a9c50d4183c7bb7cd31f5a001a9838 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 29 May 2025 10:00:43 -0700 Subject: [PATCH 1/8] add CustomElementRegistry --- src/browser/dom/document.zig | 21 ++- src/browser/env.zig | 1 + src/browser/html/elements.zig | 2 + src/browser/html/window.zig | 6 + src/browser/page.zig | 17 ++- .../webcomponents/custom_element_registry.zig | 122 ++++++++++++++++++ src/browser/webcomponents/webcomponents.zig | 23 ++++ src/runtime/js.zig | 24 ++++ 8 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/browser/webcomponents/custom_element_registry.zig create mode 100644 src/browser/webcomponents/webcomponents.zig diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index bc8dccbc..8d1e6263 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -18,6 +18,7 @@ const std = @import("std"); +const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; @@ -120,8 +121,26 @@ pub const Document = struct { return try Element.toInterface(e); } - pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion { + pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !ElementUnion { const e = try parser.documentCreateElement(self, tag_name); + + const custom_elements = &page.window.custom_elements; + if (custom_elements._get(tag_name, page)) |construct| { + var result: Env.Function.Result = undefined; + + _ = construct.newInstance(e, &result) catch |err| { + log.fatal(.user_script, "newInstance error", .{ + .err = result.exception, + .stack = result.stack, + .tag_name = tag_name, + .source = "createElement", + }); + return err; + }; + + return try Element.toInterface(e); + } + return try Element.toInterface(e); } diff --git a/src/browser/env.zig b/src/browser/env.zig index 7d6627ec..806391e5 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -33,6 +33,7 @@ const WebApis = struct { @import("xhr/xhr.zig").Interfaces, @import("xhr/form_data.zig").Interfaces, @import("xmlserializer/xmlserializer.zig").Interfaces, + @import("webcomponents/webcomponents.zig").Interfaces, }); }; diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 3ce70361..a009ec7a 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); +const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const generate = @import("../../runtime/generate.zig"); @@ -1064,6 +1065,7 @@ pub const HTMLVideoElement = struct { pub fn toInterface(comptime T: type, e: *parser.Element) !T { const elem: *align(@alignOf(*parser.Element)) parser.Element = @alignCast(e); const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem))); + return switch (tag) { .abbr, .acronym, .address, .article, .aside, .b, .basefont, .bdi, .bdo, .bgsound, .big, .center, .cite, .code, .dd, .details, .dfn, .dt, .em, .figcaption, .figure, .footer, .header, .hgroup, .i, .isindex, .keygen, .kbd, .main, .mark, .marquee, .menu, .menuitem, .nav, .nobr, .noframes, .noscript, .rp, .rt, .ruby, .s, .samp, .section, .small, .spacer, .strike, .strong, .sub, .summary, .sup, .tt, .u, .wbr, ._var => .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(elem)) }, .a => .{ .HTMLAnchorElement = @as(*parser.Anchor, @ptrCast(elem)) }, diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 02381971..e8666f3b 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -33,6 +33,7 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget; const MediaQueryList = @import("media_query_list.zig").MediaQueryList; const Performance = @import("performance.zig").Performance; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; +const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry; const storage = @import("../storage/storage.zig"); @@ -58,6 +59,7 @@ pub const Window = struct { console: Console = .{}, navigator: Navigator = .{}, performance: Performance, + custom_elements: CustomElementRegistry = .{}, pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { var fbs = std.io.fixedBufferStream(""); @@ -163,6 +165,10 @@ pub const Window = struct { return &self.performance; } + pub fn get_customElements(self: *Window) *CustomElementRegistry { + return &self.custom_elements; + } + pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 { return self.createTimeout(cbk, 5, page, .{ .animation_frame = true }); } diff --git a/src/browser/page.zig b/src/browser/page.zig index 396022db..368a3c5a 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -352,8 +352,23 @@ pub const Page = struct { continue; } - const e = parser.nodeToElement(next.?); + const current = next.?; + + const e = parser.nodeToElement(current); const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e))); + + // if (tag == .undef) { + // const tag_name = try parser.nodeLocalName(@ptrCast(e)); + // const custom_elements = &self.window.custom_elements; + // if (custom_elements._get(tag_name)) |construct| { + // try construct.printFunc(); + // // This is just here for testing for now. + // // var result: Env.Function.Result = undefined; + // // _ = try construct.newInstance(*parser.Element, &result); + // log.info(.browser, "Registered WebComponent Found", .{ .element_name = tag_name }); + // } + // } + if (tag != .script) { // ignore non-js script. continue; diff --git a/src/browser/webcomponents/custom_element_registry.zig b/src/browser/webcomponents/custom_element_registry.zig new file mode 100644 index 00000000..564d9558 --- /dev/null +++ b/src/browser/webcomponents/custom_element_registry.zig @@ -0,0 +1,122 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../../log.zig"); +const v8 = @import("v8"); + +const Env = @import("../env.zig").Env; +const Page = @import("../page.zig").Page; + +const Element = @import("../dom/element.zig").Element; + +pub const CustomElementRegistry = struct { + map: std.StringHashMapUnmanaged(v8.FunctionTemplate) = .empty, + constructors: std.StringHashMapUnmanaged(v8.Persistent(v8.Function)) = .empty, + + pub fn _define(self: *CustomElementRegistry, name: []const u8, el: Env.Function, page: *Page) !void { + log.info(.browser, "Registering WebComponent", .{ .component = name }); + + const context = page.main_context; + const duped_name = try page.arena.dupe(u8, name); + + const template = v8.FunctionTemplate.initCallback(context.isolate, struct { + fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { + const info = v8.FunctionCallbackInfo.initFromV8(raw_info); + const this = info.getThis(); + + const isolate = info.getIsolate(); + const ctx = isolate.getCurrentContext(); + + const registry_key = v8.String.initUtf8(isolate, "__lightpanda_constructor"); + const original_function = this.getValue(ctx, registry_key.toName()) catch unreachable; + if (original_function.isFunction()) { + const f = original_function.castTo(Env.Function); + f.call(void, .{}) catch unreachable; + } + } + }.callback); + + const instance_template = template.getInstanceTemplate(); + instance_template.setInternalFieldCount(1); + + const registry_key = v8.String.initUtf8(context.isolate, "__lightpanda_constructor"); + instance_template.set(registry_key.toName(), el.func, (1 << 1)); + + const class_name = v8.String.initUtf8(context.isolate, name); + template.setClassName(class_name); + + try self.map.put(page.arena, duped_name, template); + + // const entry = try self.map.getOrPut(page.arena, try page.arena.dupe(u8, name)); + // if (entry.found_existing) return error.NotSupportedError; + // entry.value_ptr.* = el; + } + + pub fn _get(self: *CustomElementRegistry, name: []const u8, page: *Page) ?Env.Function { + if (self.map.get(name)) |template| { + const func = template.getFunction(page.main_context.v8_context); + return Env.Function{ + .js_context = page.main_context, + .func = v8.Persistent(v8.Function).init(page.main_context.isolate, func), + .id = func.toObject().getIdentityHash(), + }; + } else return null; + } +}; + +const testing = @import("../../testing.zig"); + +test "Browser.CustomElementRegistry" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + try runner.testCases(&.{ + // Basic registry access + .{ "typeof customElements", "object" }, + .{ "customElements instanceof CustomElementRegistry", "true" }, + + // Define a simple custom element + .{ + \\ class MyElement { + \\ constructor() { + \\ this.textContent = 'Hello World'; + \\ } + \\ } + , + null, + }, + .{ "customElements.define('my-element', MyElement)", "undefined" }, + + // Check if element is defined + .{ "customElements.get('my-element') === MyElement", "true" }, + // .{ "customElements.get('non-existent')", "null" }, + + // Create element via document.createElement + .{ "let el = document.createElement('my-element')", "undefined" }, + // .{ "el instanceof MyElement", "true" }, + // .{ "el instanceof HTMLElement", "true" }, + // .{ "el.tagName", "MY-ELEMENT" }, + // .{ "el.textContent", "Hello World" }, + + // Create element via HTML parsing + // .{ "document.body.innerHTML = ''", "undefined" }, + // .{ "let parsed = document.querySelector('my-element')", "undefined" }, + // .{ "parsed instanceof MyElement", "true" }, + // .{ "parsed.textContent", "Hello World" }, + }, .{}); +} diff --git a/src/browser/webcomponents/webcomponents.zig b/src/browser/webcomponents/webcomponents.zig new file mode 100644 index 00000000..2ba2a30f --- /dev/null +++ b/src/browser/webcomponents/webcomponents.zig @@ -0,0 +1,23 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry; + +pub const Interfaces = .{ + CustomElementRegistry, +}; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 86619c6a..173a1dc1 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1271,6 +1271,30 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } + pub fn newInstance(self: *const Function, instance: anytype, result: *Result) !PersistentObject { + const context = self.js_context; + + var try_catch: TryCatch = undefined; + try_catch.init(context); + defer try_catch.deinit(); + + // This creates a new instance using this Function as a constructor. + // This returns a generic Object + const js_this = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse { + if (try_catch.hasCaught()) { + const allocator = context.call_arena; + result.stack = try_catch.stack(allocator) catch null; + result.exception = (try_catch.exception(allocator) catch "???") orelse "???"; + } else { + result.stack = null; + result.exception = "???"; + } + return error.JsConstructorFailed; + }; + + return try context._mapZigInstanceToJs(js_this, instance); + } + pub fn call(self: *const Function, comptime T: type, args: anytype) !T { return self.callWithThis(T, self.getThis(), args); } From f1ff7893347209c34bac6b12e9a5b714a7c3add3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 13 Jun 2025 21:16:02 +0800 Subject: [PATCH 2/8] implement custom elements - i think/hope --- src/browser/dom/document.zig | 39 +++++----- src/browser/html/elements.zig | 12 +++ .../webcomponents/custom_element_registry.zig | 73 +++++++------------ src/runtime/js.zig | 28 +++++-- 4 files changed, 80 insertions(+), 72 deletions(-) diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 8d1e6263..ea3c8e76 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -121,27 +121,28 @@ pub const Document = struct { return try Element.toInterface(e); } - pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !ElementUnion { - const e = try parser.documentCreateElement(self, tag_name); + const CreateElementResult = union(enum) { + element: ElementUnion, + custom: Env.JsObject, + }; - const custom_elements = &page.window.custom_elements; - if (custom_elements._get(tag_name, page)) |construct| { - var result: Env.Function.Result = undefined; + pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult { + const custom_element = page.window.custom_elements._get(tag_name) orelse { + const e = try parser.documentCreateElement(self, tag_name); + return .{.element = try Element.toInterface(e)}; + }; - _ = construct.newInstance(e, &result) catch |err| { - log.fatal(.user_script, "newInstance error", .{ - .err = result.exception, - .stack = result.stack, - .tag_name = tag_name, - .source = "createElement", - }); - return err; - }; - - return try Element.toInterface(e); - } - - return try Element.toInterface(e); + var result: Env.Function.Result = undefined; + const js_obj = custom_element.newInstance(&result) catch |err| { + log.fatal(.user_script, "newInstance error", .{ + .err = result.exception, + .stack = result.stack, + .tag_name = tag_name, + .source = "createElement", + }); + return err; + }; + return .{.custom = js_obj}; } pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion { diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index a009ec7a..350c51cc 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -27,6 +27,7 @@ const urlStitch = @import("../../url.zig").URL.stitch; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; const Element = @import("../dom/element.zig").Element; +const ElementUnion = @import("../dom/element.zig").Union; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; @@ -113,6 +114,16 @@ pub const HTMLElement = struct { pub const prototype = *Element; pub const subtype = .node; + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + const constructor_name = try js_this.constructorName(page.call_arena); + const tag_name = page.window.custom_elements.names.get(constructor_name) orelse { + return error.IllegalContructor; + }; + + const el = try parser.documentCreateElement(@ptrCast(page.window.document), tag_name); + return el; + } + pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration { const state = try page.getOrCreateNodeState(@ptrCast(e)); return &state.style; @@ -614,6 +625,7 @@ pub const HTMLImageElement = struct { pub const Factory = struct { pub const js_name = "Image"; pub const subtype = .node; + pub const js_legacy_factory = true; pub const prototype = *HTMLImageElement; diff --git a/src/browser/webcomponents/custom_element_registry.zig b/src/browser/webcomponents/custom_element_registry.zig index 564d9558..66abe0f6 100644 --- a/src/browser/webcomponents/custom_element_registry.zig +++ b/src/browser/webcomponents/custom_element_registry.zig @@ -26,57 +26,33 @@ const Page = @import("../page.zig").Page; const Element = @import("../dom/element.zig").Element; pub const CustomElementRegistry = struct { - map: std.StringHashMapUnmanaged(v8.FunctionTemplate) = .empty, - constructors: std.StringHashMapUnmanaged(v8.Persistent(v8.Function)) = .empty, + // JS FunctionName -> Definition Name, so that, given a function, we can + // create the element with the right tag + names: std.StringHashMapUnmanaged([]const u8) = .empty, - pub fn _define(self: *CustomElementRegistry, name: []const u8, el: Env.Function, page: *Page) !void { - log.info(.browser, "Registering WebComponent", .{ .component = name }); + // tag_name -> Function + lookup: std.StringHashMapUnmanaged(Env.Function) = .empty, - const context = page.main_context; - const duped_name = try page.arena.dupe(u8, name); + pub fn _define(self: *CustomElementRegistry, tag_name: []const u8, fun: Env.Function, page: *Page) !void { + log.info(.browser, "define custom element", .{ .name = tag_name }); - const template = v8.FunctionTemplate.initCallback(context.isolate, struct { - fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { - const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - const this = info.getThis(); + const arena = page.arena; - const isolate = info.getIsolate(); - const ctx = isolate.getCurrentContext(); + const gop = try self.lookup.getOrPut(arena, tag_name); + if (gop.found_existing) { + return error.DuplicateCustomElement; + } + errdefer _ = self.lookup.remove(tag_name); - const registry_key = v8.String.initUtf8(isolate, "__lightpanda_constructor"); - const original_function = this.getValue(ctx, registry_key.toName()) catch unreachable; - if (original_function.isFunction()) { - const f = original_function.castTo(Env.Function); - f.call(void, .{}) catch unreachable; - } - } - }.callback); + const owned_tag_name = try arena.dupe(u8, tag_name); + gop.key_ptr.* = owned_tag_name; + gop.value_ptr.* = fun; - const instance_template = template.getInstanceTemplate(); - instance_template.setInternalFieldCount(1); - - const registry_key = v8.String.initUtf8(context.isolate, "__lightpanda_constructor"); - instance_template.set(registry_key.toName(), el.func, (1 << 1)); - - const class_name = v8.String.initUtf8(context.isolate, name); - template.setClassName(class_name); - - try self.map.put(page.arena, duped_name, template); - - // const entry = try self.map.getOrPut(page.arena, try page.arena.dupe(u8, name)); - // if (entry.found_existing) return error.NotSupportedError; - // entry.value_ptr.* = el; + try self.names.putNoClobber(arena, try fun.getName(arena), owned_tag_name); } - pub fn _get(self: *CustomElementRegistry, name: []const u8, page: *Page) ?Env.Function { - if (self.map.get(name)) |template| { - const func = template.getFunction(page.main_context.v8_context); - return Env.Function{ - .js_context = page.main_context, - .func = v8.Persistent(v8.Function).init(page.main_context.isolate, func), - .id = func.toObject().getIdentityHash(), - }; - } else return null; + pub fn _get(self: *CustomElementRegistry, name: []const u8) ?Env.Function { + return self.lookup.get(name); } }; @@ -92,8 +68,9 @@ test "Browser.CustomElementRegistry" { // Define a simple custom element .{ - \\ class MyElement { + \\ class MyElement extends HTMLElement { \\ constructor() { + \\ super(); \\ this.textContent = 'Hello World'; \\ } \\ } @@ -108,10 +85,10 @@ test "Browser.CustomElementRegistry" { // Create element via document.createElement .{ "let el = document.createElement('my-element')", "undefined" }, - // .{ "el instanceof MyElement", "true" }, - // .{ "el instanceof HTMLElement", "true" }, - // .{ "el.tagName", "MY-ELEMENT" }, - // .{ "el.textContent", "Hello World" }, + .{ "el instanceof MyElement", "true" }, + .{ "el instanceof HTMLElement", "true" }, + .{ "el.tagName", "MY-ELEMENT" }, + .{ "el.textContent", "Hello World" }, // Create element via HTML parsing // .{ "document.body.innerHTML = ''", "undefined" }, diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 173a1dc1..76a5ee0a 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1257,6 +1257,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { exception: []const u8, }; + pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 { + const name = self.func.castToFunction().getName(); + return valueToString(allocator, name, self.js_context.isolate, self.js_context.v8_context); + } + pub fn withThis(self: *const Function, value: anytype) !Function { const this_obj = if (@TypeOf(value) == JsObject) value.js_obj @@ -1271,7 +1276,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } - pub fn newInstance(self: *const Function, instance: anytype, result: *Result) !PersistentObject { + pub fn newInstance(self: *const Function, result: *Result) !JsObject { const context = self.js_context; var try_catch: TryCatch = undefined; @@ -1280,7 +1285,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // This creates a new instance using this Function as a constructor. // This returns a generic Object - const js_this = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse { + const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse { if (try_catch.hasCaught()) { const allocator = context.call_arena; result.stack = try_catch.stack(allocator) catch null; @@ -1292,7 +1297,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return error.JsConstructorFailed; }; - return try context._mapZigInstanceToJs(js_this, instance); + return .{ + .js_context = context, + .js_obj = js_obj, + }; } pub fn call(self: *const Function, comptime T: type, args: anytype) !T { @@ -1474,6 +1482,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .js_obj = array.castTo(v8.Object), }; } + + pub fn constructorName(self: JsObject, allocator: Allocator) ![]const u8 { + const str = try self.js_obj.getConstructorName(); + return jsStringToZig(allocator, str, self.js_context.isolate); + } }; // This only exists so that we know whether a function wants the opaque @@ -1496,6 +1509,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { pub fn set(self: JsThis, key: []const u8, value: anytype, opts: JsObject.SetOpts) !void { return self.obj.set(key, value, opts); } + + pub fn constructorName(self: JsThis, allocator: Allocator) ![]const u8 { + return try self.obj.constructorName(allocator); + } }; pub const TryCatch = struct { @@ -1808,7 +1825,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // a constructor function, we'll return an error. if (@hasDecl(Struct, "constructor") == false) { const iso = caller.isolate; - const js_exception = iso.throwException(createException(iso, "illegal constructor")); + const js_exception = iso.throwException(createException(iso, "Illegal Constructor")); info.getReturnValue().set(js_exception); return; } @@ -2633,6 +2650,7 @@ fn Caller(comptime E: type, comptime State: type) type { var js_err: ?v8.Value = switch (err) { error.InvalidArgument => createTypeException(isolate, "invalid argument"), error.OutOfMemory => createException(isolate, "out of memory"), + error.IllegalConstructor => createException(isolate, "Illegal Contructor"), else => blk: { const func = @field(Struct, named_function.name); const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse { @@ -2745,8 +2763,8 @@ fn Caller(comptime E: type, comptime State: type) type { // a JS argument if (comptime isJsThis(params[params.len - 1].type.?)) { @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{ + .js_context = js_context, .js_obj = info.getThis(), - .executor = self.executor, } }; // AND the 2nd last parameter is state From 68dfb4ee865caf134d5e0d302a7d37d98acd5a86 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 14 Jun 2025 09:42:08 +0800 Subject: [PATCH 3/8] fix custom elements when minified js is used --- src/browser/dom/document.zig | 4 +- src/browser/html/elements.zig | 279 +++++++++++++++++- .../webcomponents/custom_element_registry.zig | 18 +- src/runtime/js.zig | 5 + 4 files changed, 284 insertions(+), 22 deletions(-) diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index ea3c8e76..fe4a499a 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -129,7 +129,7 @@ pub const Document = struct { pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult { const custom_element = page.window.custom_elements._get(tag_name) orelse { const e = try parser.documentCreateElement(self, tag_name); - return .{.element = try Element.toInterface(e)}; + return .{ .element = try Element.toInterface(e) }; }; var result: Env.Function.Result = undefined; @@ -142,7 +142,7 @@ pub const Document = struct { }); return err; }; - return .{.custom = js_obj}; + return .{ .custom = js_obj }; } pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion { diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 350c51cc..1bb5b5fa 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -115,13 +115,7 @@ pub const HTMLElement = struct { pub const subtype = .node; pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { - const constructor_name = try js_this.constructorName(page.call_arena); - const tag_name = page.window.custom_elements.names.get(constructor_name) orelse { - return error.IllegalContructor; - }; - - const el = try parser.documentCreateElement(@ptrCast(page.window.document), tag_name); - return el; + return constructHtmlElement(page, js_this); } pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration { @@ -186,6 +180,10 @@ pub const HTMLMediaElement = struct { pub const Self = parser.MediaElement; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; // HTML elements @@ -195,6 +193,10 @@ pub const HTMLUnknownElement = struct { pub const Self = parser.Unknown; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; // https://html.spec.whatwg.org/#the-a-element @@ -203,6 +205,10 @@ pub const HTMLAnchorElement = struct { pub const prototype = *HTMLElement; pub const subtype = .node; + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } + pub fn get_target(self: *parser.Anchor) ![]const u8 { return try parser.anchorGetTarget(self); } @@ -440,144 +446,240 @@ pub const HTMLAppletElement = struct { pub const Self = parser.Applet; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLAreaElement = struct { pub const Self = parser.Area; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLAudioElement = struct { pub const Self = parser.Audio; pub const prototype = *HTMLMediaElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLBRElement = struct { pub const Self = parser.BR; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLBaseElement = struct { pub const Self = parser.Base; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLBodyElement = struct { pub const Self = parser.Body; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLButtonElement = struct { pub const Self = parser.Button; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLCanvasElement = struct { pub const Self = parser.Canvas; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDListElement = struct { pub const Self = parser.DList; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDataElement = struct { pub const Self = parser.Data; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDataListElement = struct { pub const Self = parser.DataList; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDialogElement = struct { pub const Self = parser.Dialog; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDirectoryElement = struct { pub const Self = parser.Directory; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLDivElement = struct { pub const Self = parser.Div; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLEmbedElement = struct { pub const Self = parser.Embed; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLFieldSetElement = struct { pub const Self = parser.FieldSet; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLFontElement = struct { pub const Self = parser.Font; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLFrameElement = struct { pub const Self = parser.Frame; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLFrameSetElement = struct { pub const Self = parser.FrameSet; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLHRElement = struct { pub const Self = parser.HR; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLHeadElement = struct { pub const Self = parser.Head; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLHeadingElement = struct { pub const Self = parser.Heading; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLHtmlElement = struct { pub const Self = parser.Html; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLIFrameElement = struct { pub const Self = parser.IFrame; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLImageElement = struct { @@ -585,6 +687,10 @@ pub const HTMLImageElement = struct { pub const prototype = *HTMLElement; pub const subtype = .node; + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } + pub fn get_alt(self: *parser.Image) ![]const u8 { return try parser.imageGetAlt(self); } @@ -644,6 +750,10 @@ pub const HTMLInputElement = struct { pub const prototype = *HTMLElement; pub const subtype = .node; + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } + pub fn get_defaultValue(self: *parser.Input) ![]const u8 { return try parser.inputGetDefaultValue(self); } @@ -732,114 +842,190 @@ pub const HTMLLIElement = struct { pub const Self = parser.LI; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLLabelElement = struct { pub const Self = parser.Label; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLLegendElement = struct { pub const Self = parser.Legend; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLLinkElement = struct { pub const Self = parser.Link; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLMapElement = struct { pub const Self = parser.Map; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLMetaElement = struct { pub const Self = parser.Meta; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLMeterElement = struct { pub const Self = parser.Meter; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLModElement = struct { pub const Self = parser.Mod; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLOListElement = struct { pub const Self = parser.OList; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLObjectElement = struct { pub const Self = parser.Object; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLOptGroupElement = struct { pub const Self = parser.OptGroup; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLOptionElement = struct { pub const Self = parser.Option; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLOutputElement = struct { pub const Self = parser.Output; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLParagraphElement = struct { pub const Self = parser.Paragraph; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLParamElement = struct { pub const Self = parser.Param; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLPictureElement = struct { pub const Self = parser.Picture; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLPreElement = struct { pub const Self = parser.Pre; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLProgressElement = struct { pub const Self = parser.Progress; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLQuoteElement = struct { pub const Self = parser.Quote; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; // https://html.spec.whatwg.org/#the-script-element @@ -848,6 +1034,10 @@ pub const HTMLScriptElement = struct { pub const prototype = *HTMLElement; pub const subtype = .node; + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } + pub fn get_src(self: *parser.Script) !?[]const u8 { return try parser.elementGetAttribute( parser.scriptToElt(self), @@ -982,96 +1172,160 @@ pub const HTMLSourceElement = struct { pub const Self = parser.Source; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLSpanElement = struct { pub const Self = parser.Span; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLStyleElement = struct { pub const Self = parser.Style; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableElement = struct { pub const Self = parser.Table; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableCaptionElement = struct { pub const Self = parser.TableCaption; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableCellElement = struct { pub const Self = parser.TableCell; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableColElement = struct { pub const Self = parser.TableCol; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableRowElement = struct { pub const Self = parser.TableRow; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTableSectionElement = struct { pub const Self = parser.TableSection; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTemplateElement = struct { pub const Self = parser.Template; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTextAreaElement = struct { pub const Self = parser.TextArea; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTimeElement = struct { pub const Self = parser.Time; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTitleElement = struct { pub const Self = parser.Title; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLTrackElement = struct { pub const Self = parser.Track; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLUListElement = struct { pub const Self = parser.UList; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub const HTMLVideoElement = struct { pub const Self = parser.Video; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element { + return constructHtmlElement(page, js_this); + } }; pub fn toInterface(comptime T: type, e: *parser.Element) !T { @@ -1149,6 +1403,17 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T { }; } +fn constructHtmlElement(page: *Page, js_this: Env.JsThis) !*parser.Element { + const constructor_name = try js_this.constructorName(page.call_arena); + std.debug.print("constructor: {s}\n", .{constructor_name}); + if (!page.window.custom_elements.lookup.contains(constructor_name)) { + return error.IllegalContructor; + } + + const el = try parser.documentCreateElement(@ptrCast(page.window.document), constructor_name); + return el; +} + const testing = @import("../../testing.zig"); test "Browser.HTML.Element" { var runner = try testing.jsRunner(testing.tracking_allocator, .{}); diff --git a/src/browser/webcomponents/custom_element_registry.zig b/src/browser/webcomponents/custom_element_registry.zig index 66abe0f6..3e429a67 100644 --- a/src/browser/webcomponents/custom_element_registry.zig +++ b/src/browser/webcomponents/custom_element_registry.zig @@ -26,10 +26,6 @@ const Page = @import("../page.zig").Page; const Element = @import("../dom/element.zig").Element; pub const CustomElementRegistry = struct { - // JS FunctionName -> Definition Name, so that, given a function, we can - // create the element with the right tag - names: std.StringHashMapUnmanaged([]const u8) = .empty, - // tag_name -> Function lookup: std.StringHashMapUnmanaged(Env.Function) = .empty, @@ -37,18 +33,14 @@ pub const CustomElementRegistry = struct { log.info(.browser, "define custom element", .{ .name = tag_name }); const arena = page.arena; - const gop = try self.lookup.getOrPut(arena, tag_name); - if (gop.found_existing) { - return error.DuplicateCustomElement; + if (!gop.found_existing) { + errdefer _ = self.lookup.remove(tag_name); + const owned_tag_name = try arena.dupe(u8, tag_name); + gop.key_ptr.* = owned_tag_name; } - errdefer _ = self.lookup.remove(tag_name); - - const owned_tag_name = try arena.dupe(u8, tag_name); - gop.key_ptr.* = owned_tag_name; gop.value_ptr.* = fun; - - try self.names.putNoClobber(arena, try fun.getName(arena), owned_tag_name); + fun.setName(tag_name); } pub fn _get(self: *CustomElementRegistry, name: []const u8) ?Env.Function { diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 76a5ee0a..5f6e95b4 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1262,6 +1262,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return valueToString(allocator, name, self.js_context.isolate, self.js_context.v8_context); } + pub fn setName(self: *const Function, name: []const u8) void { + const v8_name = v8.String.initUtf8(self.js_context.isolate, name); + self.func.castToFunction().setName(v8_name); + } + pub fn withThis(self: *const Function, value: anytype) !Function { const this_obj = if (@TypeOf(value) == JsObject) value.js_obj From 72915760c4a96ff67726d0ceee800a5ab737b0da Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Jun 2025 13:39:44 +0800 Subject: [PATCH 4/8] Use css.querySelectorAll to find form elements Libdom's formGetCollection doesn't work (like I would expect) for dynamically added elements. For example, given: ``` let el = document.createElement('input'); document.getElementsByTagName('form')[0].append(el); ``` (and assume the page has a form), I'd expect `el.form` to be equal to the form the input was added to. Instead, it's null. This is a problem given that `dom_html_form_element_get_elements` uses the element's `form` attribute to "collect" the elements. This uses our existing querySelector to find the form elements. --- src/browser/xhr/form_data.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index 485ef647..ed4782ed 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -115,17 +115,16 @@ const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator // TODO: handle disabled fieldsets fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List { const arena = page.arena; - const collection = try parser.formGetCollection(form); - const len = try parser.htmlCollectionGetLength(collection); + const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @alignCast(@ptrCast(form)), "input,select,button,textarea"); + const nodes = node_list.nodes.items; var entries: kv.List = .{}; - try entries.ensureTotalCapacity(arena, len); + try entries.ensureTotalCapacity(arena, nodes.len); var submitter_included = false; const submitter_name_ = try getSubmitterName(submitter_); - for (0..len) |i| { - const node = try parser.htmlCollectionItem(collection, @intCast(i)); + for (nodes) |node| { const element = parser.nodeToElement(node); // must have a name @@ -181,10 +180,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page submitter_included = true; } }, - else => { - log.warn(.web_api, "unsupported form element", .{ .tag = @tagName(tag) }); - continue; - }, + else => unreachable, } } @@ -297,6 +293,7 @@ test "Browser.FormData" { \\ \\ \\ + \\ }); defer runner.deinit(); @@ -356,6 +353,8 @@ test "Browser.FormData" { try runner.testCases(&.{ .{ "let form1 = document.getElementById('form1')", null }, + .{ "let input = document.createElement('input');", null }, + .{ "input.name = 'dyn'; input.value= 'dyn-v'; form1.appendChild(input);", null }, .{ "let submit1 = document.getElementById('s1')", null }, .{ "let f2 = new FormData(form1, submit1)", null }, .{ "acc = '';", null }, @@ -378,6 +377,7 @@ test "Browser.FormData" { \\mlt-2=water \\mlt-2=tea \\s1=s1-v + \\dyn=dyn-v }, }, .{}); } From efc7b9d4a565485a9db35d8c8c165cbdbfe6bfe3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Jun 2025 19:56:19 +0800 Subject: [PATCH 5/8] Add comment explaining why we're walking the form the way we are --- src/browser/xhr/form_data.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index ed4782ed..48e03ad3 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -115,6 +115,14 @@ const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator // TODO: handle disabled fieldsets fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List { const arena = page.arena; + + // Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements) + // It doesn't work with dynamically added elements, because their form + // property doesn't get set. We should fix that. + // However, even once fixed, there are other form-collection features we + // probably want to implement (like disabled fieldsets), so we might want + // to stick with our own walker even if fix libdom to properly support + // dynamically added elements. const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @alignCast(@ptrCast(form)), "input,select,button,textarea"); const nodes = node_list.nodes.items; From e2542f41b52df962779d25cc59bfb8aace7e3aeb Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Jun 2025 19:50:13 +0800 Subject: [PATCH 6/8] Improve build and test speed Test speed has been improved only slightly by tweaking a 2-second running tests. Build has been improved by: 1 - moving logFunctionCallError out of js.Caller and to a standalone function 2 - removing some non-generic code from the generic portions of the logger Caller.getter and Caller.setter have been removed in favor or calling Caller.method. This wasn't previously possible - prior to our v8 upgrade, they had different signatures. Also removed a largely unused parser/str.zig file. --- src/browser/cssom/css_style_declaration.zig | 2 +- src/browser/html/window.zig | 11 +- src/cdp/cdp.zig | 33 ++--- src/http/client.zig | 2 +- src/log.zig | 29 +++-- src/runtime/js.zig | 130 ++++++-------------- src/str/parser.zig | 125 ------------------- 7 files changed, 78 insertions(+), 254 deletions(-) delete mode 100644 src/str/parser.zig diff --git a/src/browser/cssom/css_style_declaration.zig b/src/browser/cssom/css_style_declaration.zig index 1e980fb2..98a9015d 100644 --- a/src/browser/cssom/css_style_declaration.zig +++ b/src/browser/cssom/css_style_declaration.zig @@ -85,7 +85,7 @@ pub const CSSStyleDeclaration = struct { return self.order.items.len; } - pub fn get_parentRule() ?CSSRule { + pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule { return null; } diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index e8666f3b..74fa28b3 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -343,20 +343,15 @@ test "Browser.HTML.Window" { // Note however that we in this test do not wait as the request is just send to the browser try runner.testCases(&.{ .{ - \\ let start; + \\ let start = 0; \\ function step(timestamp) { - \\ if (start === undefined) { - \\ start = timestamp; - \\ } - \\ const elapsed = timestamp - start; - \\ if (elapsed < 2000) { - \\ requestAnimationFrame(step); - \\ } + \\ start = timestamp; \\ } , null, }, .{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test + .{ " start > 0", "true" }, }, .{}); // cancelAnimationFrame should be able to cancel a request with the given id diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 3b0005f4..30b05499 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -23,7 +23,6 @@ const json = std.json; const log = @import("../log.zig"); const App = @import("../app.zig").App; const Env = @import("../browser/env.zig").Env; -const asUint = @import("../str/parser.zig").asUint; const Browser = @import("../browser/browser.zig").Browser; const Session = @import("../browser/session.zig").Session; const Page = @import("../browser/page.zig").Page; @@ -182,41 +181,41 @@ pub fn CDPT(comptime TypeProvider: type) type { switch (domain.len) { 3 => switch (@as(u24, @bitCast(domain[0..3].*))) { - asUint("DOM") => return @import("domains/dom.zig").processMessage(command), - asUint("Log") => return @import("domains/log.zig").processMessage(command), - asUint("CSS") => return @import("domains/css.zig").processMessage(command), + asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command), + asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command), + asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command), else => {}, }, 4 => switch (@as(u32, @bitCast(domain[0..4].*))) { - asUint("Page") => return @import("domains/page.zig").processMessage(command), + asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command), else => {}, }, 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { - asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command), - asUint("Input") => return @import("domains/input.zig").processMessage(command), + asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command), + asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), else => {}, }, 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { - asUint("Target") => return @import("domains/target.zig").processMessage(command), + asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command), else => {}, }, 7 => switch (@as(u56, @bitCast(domain[0..7].*))) { - asUint("Browser") => return @import("domains/browser.zig").processMessage(command), - asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command), - asUint("Network") => return @import("domains/network.zig").processMessage(command), + asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command), + asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command), + asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command), else => {}, }, 8 => switch (@as(u64, @bitCast(domain[0..8].*))) { - asUint("Security") => return @import("domains/security.zig").processMessage(command), + asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command), else => {}, }, 9 => switch (@as(u72, @bitCast(domain[0..9].*))) { - asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command), - asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command), + asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command), + asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command), else => {}, }, 11 => switch (@as(u88, @bitCast(domain[0..11].*))) { - asUint("Performance") => return @import("domains/performance.zig").processMessage(command), + asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command), else => {}, }, else => {}, @@ -696,6 +695,10 @@ const InputParams = struct { } }; +fn asUint(comptime T: type, comptime string: []const u8) T { + return @bitCast(string[0..string.len].*); +} + const testing = @import("testing.zig"); test "cdp: invalid json" { var ctx = testing.context(); diff --git a/src/http/client.zig b/src/http/client.zig index c6cf71c0..385e8e0a 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -3170,7 +3170,7 @@ test "HttpClient: async tls no body" { } } -test "HttpClient: async tls with body x" { +test "HttpClient: async tls with body" { defer testing.reset(); for (0..5) |_| { var client = try testClient(); diff --git a/src/log.zig b/src/log.zig index 93df4c48..e00621ee 100644 --- a/src/log.zig +++ b/src/log.zig @@ -146,6 +146,16 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an } fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void { + try logLogFmtPrefix(scope, level, msg, writer); + inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { + const key = " " ++ f.name ++ "="; + try writer.writeAll(key); + try writeValue(.logfmt, @field(data, f.name), writer); + } + try writer.writeByte('\n'); +} + +fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void { try writer.writeAll("$time="); try writer.print("{d}", .{timestamp()}); @@ -164,15 +174,20 @@ fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data break :blk prefix ++ "\"" ++ msg ++ "\""; }; try writer.writeAll(full_msg); +} + +fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void { + try logPrettyPrefix(scope, level, msg, writer); inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { - const key = " " ++ f.name ++ "="; + const key = " " ++ f.name ++ " = "; try writer.writeAll(key); - try writeValue(.logfmt, @field(data, f.name), writer); + try writeValue(.pretty, @field(data, f.name), writer); + try writer.writeByte('\n'); } try writer.writeByte('\n'); } -fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void { +fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void { if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) { try writer.writeAll("\x1b[0;104mWARN "); } else { @@ -201,14 +216,6 @@ fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data try writer.print(" \x1b[0m[+{d}ms]", .{elapsed()}); try writer.writeByte('\n'); } - - inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| { - const key = " " ++ f.name ++ " = "; - try writer.writeAll(key); - try writeValue(.pretty, @field(data, f.name), writer); - try writer.writeByte('\n'); - } - try writer.writeByte('\n'); } pub fn writeValue(comptime format: Format, value: anytype, writer: anytype) !void { diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 5f6e95b4..5d047473 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1922,7 +1922,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "get_" ++ name); - caller.getter(Struct, named_function, info) catch |err| { + caller.method(Struct, named_function, info) catch |err| { caller.handleError(Struct, named_function, err, info); }; } @@ -1937,13 +1937,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); + std.debug.assert(info.length() == 1); + var caller = Caller(Self, State).init(info); defer caller.deinit(); - std.debug.assert(info.length() == 1); - const js_value = info.getArg(0); const named_function = comptime NamedFunction.init(Struct, "set_" ++ name); - caller.setter(Struct, named_function, js_value, info) catch |err| { + caller.method(Struct, named_function, info) catch |err| { caller.handleError(Struct, named_function, err, info); }; } @@ -2470,66 +2470,6 @@ fn Caller(comptime E: type, comptime State: type) type { info.getReturnValue().set(try js_context.zigValueToJs(res)); } - fn getter(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void { - const js_context = self.js_context; - const func = @field(Struct, named_function.name); - const Getter = @TypeOf(func); - if (@typeInfo(Getter).@"fn".return_type == null) { - @compileError(@typeName(Struct) ++ " has a getter without a return type: " ++ @typeName(Getter)); - } - - var args: ParamterTypes(Getter) = undefined; - const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; - switch (arg_fields.len) { - 0 => {}, // getters _can_ be parameterless - 1, 2 => { - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); - comptime assertSelfReceiver(Struct, named_function); - @field(args, "0") = zig_instance; - if (comptime arg_fields.len == 2) { - comptime assertIsStateArg(Struct, named_function, 1); - @field(args, "1") = js_context.state; - } - }, - else => @compileError(named_function.full_name + " has too many parmaters: " ++ @typeName(named_function.func)), - } - const res = @call(.auto, func, args); - info.getReturnValue().set(try js_context.zigValueToJs(res)); - } - - fn setter(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, js_value: v8.Value, info: v8.FunctionCallbackInfo) !void { - const js_context = self.js_context; - const func = @field(Struct, named_function.name); - comptime assertSelfReceiver(Struct, named_function); - - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); - - const Setter = @TypeOf(func); - var args: ParamterTypes(Setter) = undefined; - const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; - switch (arg_fields.len) { - 0 => unreachable, // assertSelfReceiver make sure of this - 1 => @compileError(named_function.full_name ++ " only has 1 parameter"), - 2, 3 => { - @field(args, "0") = zig_instance; - @field(args, "1") = try js_context.jsValueToZig(named_function, arg_fields[1].type, js_value); - if (comptime arg_fields.len == 3) { - comptime assertIsStateArg(Struct, named_function, 2); - @field(args, "2") = js_context.state; - } - }, - else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"), - } - - if (@typeInfo(Setter).@"fn".return_type) |return_type| { - if (@typeInfo(return_type) == .error_union) { - _ = try @call(.auto, func, args); - return; - } - } - _ = @call(.auto, func, args); - } - fn getIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 { const js_context = self.js_context; const func = @field(Struct, named_function.name); @@ -2642,13 +2582,7 @@ fn Caller(comptime E: type, comptime State: type) type { if (comptime builtin.mode == .Debug and @hasDecl(@TypeOf(info), "length")) { if (log.enabled(.js, .warn)) { - const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args"; - log.warn(.js, "function call error", .{ - .name = named_function.full_name, - .err = err, - .args = args_dump, - .stack = stackForLogs(self.call_arena, isolate) catch |err1| @errorName(err1), - }); + logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info); } } @@ -2717,6 +2651,7 @@ fn Caller(comptime E: type, comptime State: type) type { // Does the error we want to return belong to the custom exeception's ErrorSet fn isErrorSetException(comptime Exception: type, err: anytype) bool { const Entry = std.meta.Tuple(&.{ []const u8, void }); + const error_set = @typeInfo(Exception.ErrorSet).error_set.?; const entries = comptime blk: { var kv: [error_set.len]Entry = undefined; @@ -2855,28 +2790,6 @@ fn Caller(comptime E: type, comptime State: type) type { const Const_State = if (ti == .pointer) *const ti.pointer.child else State; return T == State or T == Const_State; } - - fn serializeFunctionArgs(self: *const Self, info: anytype) ![]const u8 { - const isolate = self.isolate; - const v8_context = self.v8_context; - const arena = self.call_arena; - const separator = log.separator(); - const js_parameter_count = info.length(); - - var arr: std.ArrayListUnmanaged(u8) = .{}; - for (0..js_parameter_count) |i| { - const js_value = info.getArg(@intCast(i)); - const value_string = try valueToDetailString(arena, js_value, isolate, v8_context); - const value_type = try jsStringToZig(arena, try js_value.typeOf(isolate), isolate); - try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{ - separator, - i + 1, - value_string, - value_type, - }); - } - return arr.items; - } }; } @@ -3317,6 +3230,37 @@ const NamedFunction = struct { } }; +// This is extracted to speed up compilation. When left inlined in handleError, +// this can add as much as 10 seconds of compilation time. +fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void { + const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args"; + log.warn(.js, "function call error", .{ + .name = function_name, + .err = err, + .args = args_dump, + .stack = stackForLogs(arena, isolate) catch |err1| @errorName(err1), + }); +} + +fn serializeFunctionArgs(arena: Allocator, isolate: v8.Isolate, context: v8.Context, info: v8.FunctionCallbackInfo) ![]const u8 { + const separator = log.separator(); + const js_parameter_count = info.length(); + + var arr: std.ArrayListUnmanaged(u8) = .{}; + for (0..js_parameter_count) |i| { + const js_value = info.getArg(@intCast(i)); + const value_string = try valueToDetailString(arena, js_value, isolate, context); + const value_type = try jsStringToZig(arena, try js_value.typeOf(isolate), isolate); + try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{ + separator, + i + 1, + value_string, + value_type, + }); + } + return arr.items; +} + // This is called from V8. Whenever the v8 inspector has to describe a value // it'll call this function to gets its [optional] subtype - which, from V8's // point of view, is an arbitrary string. diff --git a/src/str/parser.zig b/src/str/parser.zig deleted file mode 100644 index b2429f81..00000000 --- a/src/str/parser.zig +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// some utils to parser strings. -const std = @import("std"); - -pub const Reader = struct { - pos: usize = 0, - data: []const u8, - - pub fn until(self: *Reader, c: u8) []const u8 { - const pos = self.pos; - const data = self.data; - - const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len; - self.pos = index; - return data[pos..index]; - } - - pub fn tail(self: *Reader) []const u8 { - const pos = self.pos; - const data = self.data; - if (pos > data.len) { - return ""; - } - self.pos = data.len; - return data[pos..]; - } - - pub fn skip(self: *Reader) bool { - const pos = self.pos; - if (pos >= self.data.len) { - return false; - } - self.pos = pos + 1; - return true; - } -}; - -// converts a comptime-known string (i.e. null terminated) to an uint -pub fn asUint(comptime string: anytype) AsUintReturn(string) { - const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1; - const expectedType = *const [byteLength:0]u8; - if (@TypeOf(string) != expectedType) { - @compileError("expected : " ++ @typeName(expectedType) ++ - ", got: " ++ @typeName(@TypeOf(string))); - } - - return @bitCast(@as(*const [byteLength]u8, string).*); -} - -fn AsUintReturn(comptime string: anytype) type { - return @Type(.{ - .int = .{ - .bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 - .signedness = .unsigned, - }, - }); -} - -const testing = std.testing; -test "parser.Reader: skip" { - var r = Reader{ .data = "foo" }; - try testing.expectEqual(true, r.skip()); - try testing.expectEqual(true, r.skip()); - try testing.expectEqual(true, r.skip()); - try testing.expectEqual(false, r.skip()); - try testing.expectEqual(false, r.skip()); -} - -test "parser.Reader: tail" { - var r = Reader{ .data = "foo" }; - try testing.expectEqualStrings("foo", r.tail()); - try testing.expectEqualStrings("", r.tail()); - try testing.expectEqualStrings("", r.tail()); -} - -test "parser.Reader: until" { - var r = Reader{ .data = "foo.bar.baz" }; - try testing.expectEqualStrings("foo", r.until('.')); - _ = r.skip(); - try testing.expectEqualStrings("bar", r.until('.')); - _ = r.skip(); - try testing.expectEqualStrings("baz", r.until('.')); - - r = Reader{ .data = "foo" }; - try testing.expectEqualStrings("foo", r.until('.')); - try testing.expectEqualStrings("", r.tail()); - - r = Reader{ .data = "" }; - try testing.expectEqualStrings("", r.until('.')); - try testing.expectEqualStrings("", r.tail()); -} - -test "parser: asUint" { - const ASCII_x = @as(u8, @bitCast([1]u8{'x'})); - const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' })); - const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' })); - const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' })); - - try testing.expectEqual(ASCII_x, asUint("x")); - try testing.expectEqual(ASCII_ab, asUint("ab")); - try testing.expectEqual(ASCII_xyz, asUint("xyz")); - try testing.expectEqual(ASCII_abcd, asUint("abcd")); - - try testing.expectEqual(u8, @TypeOf(asUint("x"))); - try testing.expectEqual(u16, @TypeOf(asUint("ab"))); - try testing.expectEqual(u24, @TypeOf(asUint("xyz"))); - try testing.expectEqual(u32, @TypeOf(asUint("abcd"))); -} From 329bffb127c356180dd8772294cc039aabd21baa Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Jun 2025 17:07:43 +0800 Subject: [PATCH 7/8] Fix non-probing of union array failure Probing a union match for an possible value should rarely hard-fail. Instead, it should return an .{.invalid = {}} response to let the prober decide how to proceed. This fixes a hard-fail when a JS value fails to probe as an array. Also, add :modal pseudo-class. (both issues came up looking at github integration) --- src/browser/css/parser.zig | 1 + src/browser/css/selector.zig | 2 ++ src/runtime/js.zig | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/css/parser.zig b/src/browser/css/parser.zig index 70fe20a0..4bcf7f9f 100644 --- a/src/browser/css/parser.zig +++ b/src/browser/css/parser.zig @@ -605,6 +605,7 @@ pub const Parser = struct { .after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class }, .first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class }, .selection, .spelling_error => return .{ .pseudo_element = pseudo_class }, + .modal => return .{ .pseudo_element = pseudo_class }, } } diff --git a/src/browser/css/selector.zig b/src/browser/css/selector.zig index a3e37246..301af641 100644 --- a/src/browser/css/selector.zig +++ b/src/browser/css/selector.zig @@ -98,6 +98,7 @@ pub const PseudoClass = enum { placeholder, selection, spelling_error, + modal, pub const Error = error{ InvalidPseudoClass, @@ -154,6 +155,7 @@ pub const PseudoClass = enum { if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder; if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection; if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error; + if (std.ascii.eqlIgnoreCase(s, "modal")) return .modal; return Error.InvalidPseudoClass; } }; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 5d047473..083b86e3 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1158,7 +1158,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } if (!js_value.isArray()) { - return error.InvalidArgument; + return .{.invalid = {}}; } // This can get tricky. From 9f54cb35f432517c84127f708aa6893041aafe95 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 17 Jun 2025 08:20:11 +0800 Subject: [PATCH 8/8] remove unused import and debug stmt --- src/browser/html/elements.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 1bb5b5fa..948810a2 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -27,7 +27,6 @@ const urlStitch = @import("../../url.zig").URL.stitch; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; const Element = @import("../dom/element.zig").Element; -const ElementUnion = @import("../dom/element.zig").Union; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; @@ -1405,7 +1404,6 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T { fn constructHtmlElement(page: *Page, js_this: Env.JsThis) !*parser.Element { const constructor_name = try js_this.constructorName(page.call_arena); - std.debug.print("constructor: {s}\n", .{constructor_name}); if (!page.window.custom_elements.lookup.contains(constructor_name)) { return error.IllegalContructor; }