implement custom elements - i think/hope

This commit is contained in:
Karl Seguin
2025-06-13 21:16:02 +08:00
committed by Muki Kiboigo
parent 1f45d5b8e4
commit f1ff789334
4 changed files with 80 additions and 72 deletions

View File

@@ -121,27 +121,28 @@ pub const Document = struct {
return try Element.toInterface(e); return try Element.toInterface(e);
} }
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !ElementUnion { const CreateElementResult = union(enum) {
const e = try parser.documentCreateElement(self, tag_name); element: ElementUnion,
custom: Env.JsObject,
};
const custom_elements = &page.window.custom_elements; pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
if (custom_elements._get(tag_name, page)) |construct| { const custom_element = page.window.custom_elements._get(tag_name) orelse {
var result: Env.Function.Result = undefined; const e = try parser.documentCreateElement(self, tag_name);
return .{.element = try Element.toInterface(e)};
};
_ = construct.newInstance(e, &result) catch |err| { var result: Env.Function.Result = undefined;
log.fatal(.user_script, "newInstance error", .{ const js_obj = custom_element.newInstance(&result) catch |err| {
.err = result.exception, log.fatal(.user_script, "newInstance error", .{
.stack = result.stack, .err = result.exception,
.tag_name = tag_name, .stack = result.stack,
.source = "createElement", .tag_name = tag_name,
}); .source = "createElement",
return err; });
}; return err;
};
return try Element.toInterface(e); return .{.custom = js_obj};
}
return try Element.toInterface(e);
} }
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion { pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {

View File

@@ -27,6 +27,7 @@ const urlStitch = @import("../../url.zig").URL.stitch;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
const Element = @import("../dom/element.zig").Element; const Element = @import("../dom/element.zig").Element;
const ElementUnion = @import("../dom/element.zig").Union;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
@@ -113,6 +114,16 @@ pub const HTMLElement = struct {
pub const prototype = *Element; pub const prototype = *Element;
pub const subtype = .node; 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 { pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration {
const state = try page.getOrCreateNodeState(@ptrCast(e)); const state = try page.getOrCreateNodeState(@ptrCast(e));
return &state.style; return &state.style;
@@ -614,6 +625,7 @@ pub const HTMLImageElement = struct {
pub const Factory = struct { pub const Factory = struct {
pub const js_name = "Image"; pub const js_name = "Image";
pub const subtype = .node; pub const subtype = .node;
pub const js_legacy_factory = true; pub const js_legacy_factory = true;
pub const prototype = *HTMLImageElement; pub const prototype = *HTMLImageElement;

View File

@@ -26,57 +26,33 @@ const Page = @import("../page.zig").Page;
const Element = @import("../dom/element.zig").Element; const Element = @import("../dom/element.zig").Element;
pub const CustomElementRegistry = struct { pub const CustomElementRegistry = struct {
map: std.StringHashMapUnmanaged(v8.FunctionTemplate) = .empty, // JS FunctionName -> Definition Name, so that, given a function, we can
constructors: std.StringHashMapUnmanaged(v8.Persistent(v8.Function)) = .empty, // 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 { // tag_name -> Function
log.info(.browser, "Registering WebComponent", .{ .component = name }); lookup: std.StringHashMapUnmanaged(Env.Function) = .empty,
const context = page.main_context; pub fn _define(self: *CustomElementRegistry, tag_name: []const u8, fun: Env.Function, page: *Page) !void {
const duped_name = try page.arena.dupe(u8, name); log.info(.browser, "define custom element", .{ .name = tag_name });
const template = v8.FunctionTemplate.initCallback(context.isolate, struct { const arena = page.arena;
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 gop = try self.lookup.getOrPut(arena, tag_name);
const ctx = isolate.getCurrentContext(); if (gop.found_existing) {
return error.DuplicateCustomElement;
}
errdefer _ = self.lookup.remove(tag_name);
const registry_key = v8.String.initUtf8(isolate, "__lightpanda_constructor"); const owned_tag_name = try arena.dupe(u8, tag_name);
const original_function = this.getValue(ctx, registry_key.toName()) catch unreachable; gop.key_ptr.* = owned_tag_name;
if (original_function.isFunction()) { gop.value_ptr.* = fun;
const f = original_function.castTo(Env.Function);
f.call(void, .{}) catch unreachable;
}
}
}.callback);
const instance_template = template.getInstanceTemplate(); try self.names.putNoClobber(arena, try fun.getName(arena), owned_tag_name);
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 { pub fn _get(self: *CustomElementRegistry, name: []const u8) ?Env.Function {
if (self.map.get(name)) |template| { return self.lookup.get(name);
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;
} }
}; };
@@ -92,8 +68,9 @@ test "Browser.CustomElementRegistry" {
// Define a simple custom element // Define a simple custom element
.{ .{
\\ class MyElement { \\ class MyElement extends HTMLElement {
\\ constructor() { \\ constructor() {
\\ super();
\\ this.textContent = 'Hello World'; \\ this.textContent = 'Hello World';
\\ } \\ }
\\ } \\ }
@@ -108,10 +85,10 @@ test "Browser.CustomElementRegistry" {
// Create element via document.createElement // Create element via document.createElement
.{ "let el = document.createElement('my-element')", "undefined" }, .{ "let el = document.createElement('my-element')", "undefined" },
// .{ "el instanceof MyElement", "true" }, .{ "el instanceof MyElement", "true" },
// .{ "el instanceof HTMLElement", "true" }, .{ "el instanceof HTMLElement", "true" },
// .{ "el.tagName", "MY-ELEMENT" }, .{ "el.tagName", "MY-ELEMENT" },
// .{ "el.textContent", "Hello World" }, .{ "el.textContent", "Hello World" },
// Create element via HTML parsing // Create element via HTML parsing
// .{ "document.body.innerHTML = '<my-element></my-element>'", "undefined" }, // .{ "document.body.innerHTML = '<my-element></my-element>'", "undefined" },

View File

@@ -1257,6 +1257,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
exception: []const u8, 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 { pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == JsObject) const this_obj = if (@TypeOf(value) == JsObject)
value.js_obj 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; const context = self.js_context;
var try_catch: TryCatch = undefined; 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 creates a new instance using this Function as a constructor.
// This returns a generic Object // 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()) { if (try_catch.hasCaught()) {
const allocator = context.call_arena; const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null; 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 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 { 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), .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 // 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 { pub fn set(self: JsThis, key: []const u8, value: anytype, opts: JsObject.SetOpts) !void {
return self.obj.set(key, value, opts); 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 { 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. // a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) { if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate; 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); info.getReturnValue().set(js_exception);
return; return;
} }
@@ -2633,6 +2650,7 @@ fn Caller(comptime E: type, comptime State: type) type {
var js_err: ?v8.Value = switch (err) { var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"), error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => createException(isolate, "out of memory"), error.OutOfMemory => createException(isolate, "out of memory"),
error.IllegalConstructor => createException(isolate, "Illegal Contructor"),
else => blk: { else => blk: {
const func = @field(Struct, named_function.name); const func = @field(Struct, named_function.name);
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse { 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 // a JS argument
if (comptime isJsThis(params[params.len - 1].type.?)) { if (comptime isJsThis(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{ @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
.js_context = js_context,
.js_obj = info.getThis(), .js_obj = info.getThis(),
.executor = self.executor,
} }; } };
// AND the 2nd last parameter is state // AND the 2nd last parameter is state