Skip to content

Commit 818f454

Browse files
committed
Add basic ShadowRoot implementation, polyfill webcomponents
1 parent d6ace3f commit 818f454

File tree

14 files changed

+242
-355
lines changed

14 files changed

+242
-355
lines changed

src/browser/State.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
const Env = @import("env.zig").Env;
3030
const parser = @import("netsurf.zig");
3131
const DataSet = @import("html/DataSet.zig");
32+
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
3233
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
3334

3435
// for HTMLScript (but probably needs to be added to more)
@@ -62,6 +63,8 @@ explicit_index_set: bool = false,
6263

6364
template_content: ?*parser.DocumentFragment = null,
6465

66+
shadow_root: ?*ShadowRoot = null,
67+
6568
const ReadyState = enum {
6669
loading,
6770
interactive,

src/browser/dom/document.zig

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,28 +123,11 @@ pub const Document = struct {
123123
return try Element.toInterface(e);
124124
}
125125

126-
const CreateElementResult = union(enum) {
127-
element: ElementUnion,
128-
custom: Env.JsObject,
129-
};
130-
131-
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
132-
const custom_element = page.window.custom_elements._get(tag_name) orelse {
133-
const e = try parser.documentCreateElement(self, tag_name);
134-
return .{ .element = try Element.toInterface(e) };
135-
};
136-
137-
var result: Env.Function.Result = undefined;
138-
const js_obj = custom_element.newInstance(&result) catch |err| {
139-
log.fatal(.user_script, "newInstance error", .{
140-
.err = result.exception,
141-
.stack = result.stack,
142-
.tag_name = tag_name,
143-
.source = "createElement",
144-
});
145-
return err;
146-
};
147-
return .{ .custom = js_obj };
126+
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
127+
// The element’s namespace is the HTML namespace when document is an HTML document
128+
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
129+
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
130+
return Element.toInterface(e);
148131
}
149132

150133
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {

src/browser/dom/element.zig

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const Node = @import("node.zig").Node;
3030
const Walker = @import("walker.zig").WalkerDepthFirst;
3131
const NodeList = @import("nodelist.zig").NodeList;
3232
const HTMLElem = @import("../html/elements.zig");
33+
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
34+
3335
pub const Union = @import("../html/elements.zig").Union;
3436

3537
// WEB IDL https://dom.spec.whatwg.org/#element
@@ -459,6 +461,44 @@ pub const Element = struct {
459461
_ = opts;
460462
return true;
461463
}
464+
465+
const AttachShadowOpts = struct {
466+
mode: []const u8, // must be specified
467+
};
468+
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
469+
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
470+
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
471+
if (state.shadow_root) |sr| {
472+
if (mode != sr.mode) {
473+
// this is the behavior per the spec
474+
return error.NotSupportedError;
475+
}
476+
477+
// TODO: the existing shadow root should be cleared!
478+
return sr;
479+
}
480+
481+
// Not sure what to do if there is no owner document
482+
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
483+
const fragment = try parser.documentCreateDocumentFragment(doc);
484+
const sr = try page.arena.create(ShadowRoot);
485+
sr.* = .{
486+
.host = self,
487+
.mode = mode,
488+
.proto = fragment,
489+
};
490+
state.shadow_root = sr;
491+
return sr;
492+
}
493+
494+
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
495+
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
496+
const sr = state.shadow_root orelse return null;
497+
if (sr.mode == .closed) {
498+
return null;
499+
}
500+
return sr;
501+
}
462502
};
463503

464504
// Tests

src/browser/dom/shadow_root.zig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const parser = @import("../netsurf.zig");
21+
const Element = @import("element.zig").Element;
22+
const ElementUnion = @import("element.zig").Union;
23+
24+
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
25+
pub const ShadowRoot = struct {
26+
pub const prototype = *parser.DocumentFragment;
27+
pub const subtype = .node;
28+
29+
mode: Mode,
30+
host: *parser.Element,
31+
proto: *parser.DocumentFragment,
32+
33+
pub const Mode = enum {
34+
open,
35+
closed,
36+
};
37+
38+
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
39+
return Element.toInterface(self.host);
40+
}
41+
};
42+
43+
const testing = @import("../../testing.zig");
44+
test "Browser.DOM.ShadowRoot" {
45+
defer testing.reset();
46+
47+
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
48+
defer runner.deinit();
49+
50+
try runner.testCases(&.{
51+
.{ "const div1 = document.createElement('div');", null },
52+
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
53+
.{ "sr1.host == div1", "true" },
54+
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
55+
.{ "div1.shadowRoot == sr1", "true" },
56+
57+
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
58+
}, .{});
59+
60+
try runner.testCases(&.{
61+
.{ "const div2 = document.createElement('di2');", null },
62+
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
63+
.{ "sr2.host == div2", "true" },
64+
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
65+
}, .{});
66+
}

src/browser/env.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const WebApis = struct {
2525
@import("css/css.zig").Interfaces,
2626
@import("cssom/cssom.zig").Interfaces,
2727
@import("dom/dom.zig").Interfaces,
28+
@import("dom/shadow_root.zig").ShadowRoot,
2829
@import("encoding/text_encoder.zig").Interfaces,
2930
@import("events/event.zig").Interfaces,
3031
@import("html/html.zig").Interfaces,
@@ -34,7 +35,6 @@ const WebApis = struct {
3435
@import("xhr/xhr.zig").Interfaces,
3536
@import("xhr/form_data.zig").Interfaces,
3637
@import("xmlserializer/xmlserializer.zig").Interfaces,
37-
@import("webcomponents/webcomponents.zig").Interfaces,
3838
});
3939
};
4040

0 commit comments

Comments
 (0)