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