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); }