mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
add CustomElementRegistry
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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)) },
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
122
src/browser/webcomponents/custom_element_registry.zig
Normal file
122
src/browser/webcomponents/custom_element_registry.zig
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 = '<my-element></my-element>'", "undefined" },
|
||||
// .{ "let parsed = document.querySelector('my-element')", "undefined" },
|
||||
// .{ "parsed instanceof MyElement", "true" },
|
||||
// .{ "parsed.textContent", "Hello World" },
|
||||
}, .{});
|
||||
}
|
||||
23
src/browser/webcomponents/webcomponents.zig
Normal file
23
src/browser/webcomponents/webcomponents.zig
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
const CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CustomElementRegistry,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user