Merge pull request #2556 from lightpanda-io/declarative_shadow_dom

Add declarative shadow dom (DSD)
This commit is contained in:
Karl Seguin
2026-05-28 16:36:11 +08:00
committed by GitHub
17 changed files with 382 additions and 43 deletions

View File

@@ -576,7 +576,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
};
const parse_arena = try self.getArena(.medium, "Frame.parseBlob");
defer self.releaseArena(parse_arena);
var parser = Parser.init(parse_arena, self.document.asNode(), self);
var parser = Parser.init(parse_arena, self.document.asNode(), self, .{ .allow_declarative_shadow = true });
parser.parse(blob._slice);
} else {
self.document.injectBlank(self) catch |err| {
@@ -1187,7 +1187,7 @@ fn frameDoneCallback(ctx: *anyopaque) !void {
const parse_arena = try self.getArena(.medium, "Frame.parse");
defer self.releaseArena(parse_arena);
var parser = Parser.init(parse_arena, self.document.asNode(), self);
var parser = Parser.init(parse_arena, self.document.asNode(), self, .{ .allow_declarative_shadow = true });
switch (self._parse_state) {
.html => |*html| {
@@ -3563,11 +3563,20 @@ pub fn updateRangesForNodeRemoval(self: *Frame, parent: *Node, child: *Node, chi
// TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '')
pub fn parseHtmlAsChildren(self: *Frame, node: *Node, html: []const u8) !void {
return self.parseHtmlAsChildrenInner(node, html, false);
}
// setHTMLUnsafe variant: parse a fragment that may contain declarative shadow node
pub fn parseHtmlUnsafeAsChildren(self: *Frame, node: *Node, html: []const u8) !void {
return self.parseHtmlAsChildrenInner(node, html, true);
}
fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, allow_declarative_shadow: bool) !void {
const previous_parse_mode = self._parse_mode;
self._parse_mode = .fragment;
defer self._parse_mode = previous_parse_mode;
var parser = Parser.init(self.call_arena, node, self);
var parser = Parser.init(self.call_arena, node, self, .{ .allow_declarative_shadow = allow_declarative_shadow });
parser.parseFragment(html);
// html5ever wraps fragment output in an <html> element; unwrap so its

View File

@@ -775,7 +775,7 @@ fn testMarkdownShadow(light: []const u8, shadow: []const u8, expected: []const u
try frame.parseHtmlAsChildren(host.asNode(), light);
}
const sr = try host.attachShadow("open", frame);
const sr = try host.attachShadow(comptime .wrap("open"), frame);
try frame.parseHtmlAsChildren(sr.asNode(), shadow);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
@@ -814,3 +814,24 @@ test "browser.markdown: slot fallback content when nothing assigned" {
\\<slot name="x">Default text</slot>
, "Default text\n");
}
// End-to-end: a declarative shadow root (parsed via setHTMLUnsafe) is attached
// as a real shadow tree, and markdown's composed-tree piercing then renders it.
test "browser.markdown: declarative shadow DOM renders through piercing" {
const testing = @import("../testing.zig");
const frame = try testing.test_session.createPage();
defer testing.test_session.removePage();
frame.url = "http://localhost/";
const doc = frame.window._document;
const host = try doc.createElement("div", null, frame);
try host.setHTMLUnsafe(
\\<div><template shadowrootmode="open"><p>shadow content</p></template></div>
, frame);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dump(host.asNode(), .{}, &aw.writer, frame);
try testing.expectString("\nshadow content\n", aw.written());
}

View File

@@ -71,7 +71,16 @@ pending_text: ?PendingText,
// second chunk of a run, so the common case stays at one copy.
buf: std.ArrayList(u8),
pub fn init(arena: Allocator, node: *Node, frame: *Frame) Parser {
// Whether `<template shadowrootmode>` is parsed into a real shadow root.
// True for document navigation, document.write, and setHTMLUnsafe; false for
// innerHTML and DOMParser (per spec). Set from Options at init.
allow_declarative_shadow: bool = false,
pub const Options = struct {
allow_declarative_shadow: bool = false,
};
pub fn init(arena: Allocator, node: *Node, frame: *Frame, opts: Options) Parser {
return .{
.err = null,
.frame = frame,
@@ -83,6 +92,7 @@ pub fn init(arena: Allocator, node: *Node, frame: *Frame) Parser {
},
.pending_text = null,
.buf = .empty,
.allow_declarative_shadow = opts.allow_declarative_shadow,
};
}
@@ -157,6 +167,7 @@ const Error = struct {
reparent_children,
append_before_sibling,
append_based_on_parent_node,
attach_declarative_shadow,
};
};
@@ -180,6 +191,8 @@ pub fn parse(self: *Parser, html: []const u8) void {
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
attachDeclarativeShadowCallback,
self.allow_declarative_shadow,
);
self.flushPendingText() catch |err| {
if (self.err == null) self.err = .{ .err = err, .source = .append };
@@ -209,6 +222,8 @@ pub fn parseWithEncoding(self: *Parser, html: []const u8, charset: []const u8) v
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
attachDeclarativeShadowCallback,
self.allow_declarative_shadow,
);
self.flushPendingText() catch |err| {
if (self.err == null) self.err = .{ .err = err, .source = .append };
@@ -235,6 +250,8 @@ pub fn parseXML(self: *Parser, xml: []const u8) void {
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
attachDeclarativeShadowCallback,
false,
);
self.flushPendingText() catch |err| {
if (self.err == null) self.err = .{ .err = err, .source = .append };
@@ -261,6 +278,8 @@ pub fn parseFragment(self: *Parser, html: []const u8) void {
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
attachDeclarativeShadowCallback,
self.allow_declarative_shadow,
);
self.flushPendingText() catch |err| {
if (self.err == null) self.err = .{ .err = err, .source = .append };
@@ -271,10 +290,10 @@ pub const Streaming = struct {
parser: Parser,
handle: ?*anyopaque,
pub fn init(arena: Allocator, node: *Node, frame: *Frame) Streaming {
pub fn init(arena: Allocator, node: *Node, frame: *Frame, opts: Options) Streaming {
return .{
.handle = null,
.parser = Parser.init(arena, node, frame),
.parser = Parser.init(arena, node, frame, opts),
};
}
@@ -304,6 +323,8 @@ pub const Streaming = struct {
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
attachDeclarativeShadowCallback,
self.parser.allow_declarative_shadow,
) orelse return error.ParserCreationFailed;
}
@@ -499,6 +520,34 @@ fn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque {
return pn;
}
// Called for `<template shadowrootmode>` when declarative shadow roots are
// allowed. Attaches a shadow root to `host` and redirects the (stack-only)
// template's contents into it, so html5ever parses the template's children
// straight into the shadow root. Returns 1 on success, 0 to tell html5ever to
// fall back to inserting the template as a normal light-DOM element.
fn attachDeclarativeShadowCallback(ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8 {
const self: *Parser = @ptrCast(@alignCast(ctx));
return self._attachDeclarativeShadowCallback(getNode(host_ref), getNode(template_ref), mode_is_open != 0) catch |err| {
self.err = .{ .err = err, .source = .attach_declarative_shadow };
return 0;
};
}
fn _attachDeclarativeShadowCallback(self: *Parser, host_node: *Node, template_node: *Node, mode_is_open: bool) !u8 {
// guaranteed by html5ever
const host = host_node.as(Element);
const mode: lp.String = if (mode_is_open) comptime .wrap("open") else comptime .wrap("closed");
const shadow = host.attachShadow(mode, self.frame) catch |err| switch (err) {
// Expected per-spec fall-backs (host can't host a shadow, or already
// has one): keep the <template> in the light DOM instead.
error.NotSupported => return 0,
else => return err,
};
const template = template_node.as(Element).is(Element.Html.Template) orelse return 0;
template._content = shadow.asDocumentFragment();
return 1;
}
fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
// For non-elements, data is null. But, we expect this to only ever

View File

@@ -37,6 +37,8 @@ pub extern "c" fn html5ever_parse_document(
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
attachDeclarativeShadowCallback: *const fn (ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8,
allow_declarative_shadow: bool,
) void;
/// Parse HTML document with encoding conversion. Converts from charset to UTF-8 before parsing.
@@ -61,6 +63,8 @@ pub extern "c" fn html5ever_parse_document_with_encoding(
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
attachDeclarativeShadowCallback: *const fn (ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8,
allow_declarative_shadow: bool,
) void;
pub extern "c" fn html5ever_parse_fragment(
@@ -82,6 +86,8 @@ pub extern "c" fn html5ever_parse_fragment(
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
attachDeclarativeShadowCallback: *const fn (ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8,
allow_declarative_shadow: bool,
) void;
pub extern "c" fn html5ever_attribute_iterator_next(ctx: *anyopaque) Nullable(Attribute);
@@ -112,6 +118,8 @@ pub extern "c" fn html5ever_streaming_parser_create(
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
attachDeclarativeShadowCallback: *const fn (ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8,
allow_declarative_shadow: bool,
) ?*anyopaque;
pub extern "c" fn html5ever_streaming_parser_feed(
@@ -215,6 +223,8 @@ pub extern "c" fn xml5ever_parse_document(
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
attachDeclarativeShadowCallback: *const fn (ctx: *anyopaque, host_ref: *anyopaque, template_ref: *anyopaque, mode_is_open: u8) callconv(.c) u8,
allow_declarative_shadow: bool,
) void;
// General encoding api

View File

@@ -142,6 +142,23 @@
}
</script>
<!-- document.write processes declarative shadow roots, like the parser. -->
<script id=write_declarative_shadow>
document.write('<div id="dw_dsd"><template shadowrootmode="open"><p>written shadow</p></template></div>');
testing.expectEqual(true, true);
</script>
<script id=verify_declarative_shadow>
{
const host = document.getElementById('dw_dsd');
// The shadow root survives the fragment->live-DOM splice.
testing.expectEqual(true, host.shadowRoot !== null);
testing.expectEqual('written shadow', host.shadowRoot.querySelector('p').textContent);
// The <template> was consumed, not left in light DOM.
testing.expectEqual(null, host.querySelector('template'));
}
</script>
<!-- Phase 3 Tests: document.open/close would go here -->
<!-- Note: Testing document.open/close requires async/setTimeout which doesn't -->
<!-- work well with the test isolation. The implementation is tested manually. -->

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="host"></div>
<!-- Declarative shadow root in the document markup itself: exercises the
document-parse path (the SSR scenario), not just setHTMLUnsafe. -->
<div id="dochost"><template shadowrootmode="open"><p>doc shadow</p></template></div>
<script id="document_parse_attaches_shadow">
{
const host = $('#dochost');
testing.expectTrue(host.shadowRoot !== null);
testing.expectEqual('doc shadow', host.shadowRoot.querySelector('p').textContent);
testing.expectEqual(null, host.querySelector('template'));
}
</script>
<script id="setHTMLUnsafe_open">
{
const host = $('#host');
host.setHTMLUnsafe('<div id="inner"><template shadowrootmode="open"><p>shadow content</p></template></div>');
const inner = host.querySelector('#inner');
// A real shadow root was attached, and no <template> was left in light DOM.
testing.expectTrue(inner.shadowRoot !== null);
testing.expectEqual('open', inner.shadowRoot.mode);
testing.expectEqual('shadow content', inner.shadowRoot.querySelector('p').textContent);
testing.expectEqual(null, inner.querySelector('template'));
}
</script>
<script id="setHTMLUnsafe_closed">
{
const host = document.createElement('div');
host.setHTMLUnsafe('<div id="inner"><template shadowrootmode="closed"><p>x</p></template></div>');
const inner = host.querySelector('#inner');
// Closed shadow root: not exposed via .shadowRoot, but the template is consumed.
testing.expectEqual(null, inner.shadowRoot);
testing.expectEqual(null, inner.querySelector('template'));
}
</script>
<script id="innerHTML_does_not_attach">
{
const host = document.createElement('div');
// innerHTML must NOT process declarative shadow roots (only setHTMLUnsafe does).
host.innerHTML = '<div id="inner"><template shadowrootmode="open"><p>x</p></template></div>';
const inner = host.querySelector('#inner');
testing.expectEqual(null, inner.shadowRoot);
testing.expectTrue(inner.querySelector('template') !== null);
}
</script>
<script id="second_declarative_template_retained">
{
const host = document.createElement('div');
host.setHTMLUnsafe('<div id="inner"><template shadowrootmode="open"><p>first</p></template><template shadowrootmode="open"><p>second</p></template></div>');
const inner = host.querySelector('#inner');
testing.expectTrue(inner.shadowRoot !== null);
testing.expectEqual('first', inner.shadowRoot.querySelector('p').textContent);
// The host already has a shadow root, so the second template stays in light DOM.
testing.expectTrue(inner.querySelector('template') !== null);
}
</script>
<script id="declarative_slot_projection">
{
const host = document.createElement('div');
host.setHTMLUnsafe('<div id="inner"><template shadowrootmode="open"><slot></slot></template><p>light</p></div>');
const inner = host.querySelector('#inner');
const slot = inner.shadowRoot.querySelector('slot');
const assigned = slot.assignedNodes();
testing.expectEqual(1, assigned.length);
testing.expectEqual('light', assigned[0].textContent);
}
</script>
<script id="attachShadow_invalid_host_throws">
{
// Host validation: <input> cannot host a shadow root.
testing.expectError('NotSupportedError', () => {
document.createElement('input').attachShadow({ mode: 'open' });
});
}
</script>
<script id="attachShadow_twice_throws">
{
const host = document.createElement('div');
host.attachShadow({ mode: 'open' });
testing.expectError('NotSupportedError', () => {
host.attachShadow({ mode: 'open' });
});
}
</script>
<script id="disabled_features_blocks_declarative_shadow">
{
class ShadowDisabledElement extends HTMLElement {
static get disabledFeatures() { return ['shadow']; }
}
customElements.define('shadow-disabled-host', ShadowDisabledElement);
const host = document.createElement('div');
host.setHTMLUnsafe('<shadow-disabled-host><template shadowrootmode="open"><span>x</span></template></shadow-disabled-host>');
const sd = host.querySelector('shadow-disabled-host');
// disabledFeatures: ['shadow'] -> declarative attach must fail, template kept.
testing.expectEqual(null, sd.shadowRoot);
testing.expectTrue(sd.querySelector('template') !== null);
}
</script>
<script id="disabled_features_blocks_imperative_shadow">
{
const sd = document.createElement('shadow-disabled-host');
testing.expectError('NotSupportedError', () => sd.attachShadow({ mode: 'open' }));
}
</script>

View File

@@ -36,6 +36,9 @@ observed_attributes: std.StringHashMapUnmanaged(void) = .{},
// For autonomous custom elements, this is null
extends: ?Element.Tag = null,
// when disabledFeatures = ["shadow"], we'll throw if attachShadow is called
disable_shadow: bool = false,
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: String) bool {
return self.observed_attributes.contains(name.str());
}

View File

@@ -81,6 +81,20 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
}
}
// Read disabledFeatures static property: ["shadow"] makes attachShadow throw.
if (constructor.getPropertyValue("disabledFeatures") catch null) |disabled| {
if (disabled.isArray()) {
var js_arr = disabled.toArray();
for (0..js_arr.len()) |i| {
const val = js_arr.get(@intCast(i)) catch continue;
const feature = val.toSSO(false) catch continue;
if (feature.eql(comptime .wrap("shadow"))) {
definition.disable_shadow = true;
}
}
}
}
gop.key_ptr.* = owned_name;
gop.value_ptr.* = definition;

View File

@@ -77,7 +77,7 @@ pub fn parseFromString(
}
// Parse HTML into the document
var parser = Parser.init(arena, doc.asNode(), frame);
var parser = Parser.init(arena, doc.asNode(), frame, .{});
parser.parse(normalized);
if (parser.err) |pe| {
@@ -94,13 +94,13 @@ pub fn parseFromString(
// Parse XML into XMLDocument.
const doc_node = doc.asNode();
var parser = Parser.init(arena, doc_node, frame);
var parser = Parser.init(arena, doc_node, frame, .{});
parser.parseXML(html);
if (parser.err != null or doc_node.firstChild() == null) {
// Return a document with a <parsererror> element per spec.
const err_doc = try frame._factory.document(XMLDocument{ ._proto = undefined });
var err_parser = Parser.init(arena, err_doc.asNode(), frame);
var err_parser = Parser.init(arena, err_doc.asNode(), frame, .{});
err_parser.parseXML("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>");
return err_doc.asDocument();
}

View File

@@ -786,7 +786,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
const arena = try frame.getArena(.medium, "Document.write");
defer frame.releaseArena(arena);
var parser = Parser.init(arena, fragment_node, frame);
var parser = Parser.init(arena, fragment_node, frame, .{ .allow_declarative_shadow = true });
parser.parseFragment(html);
// Extract children from wrapper HTML element (html5ever wraps fragments)
@@ -873,7 +873,7 @@ pub fn open(self: *Document, call_frame: *Frame) !*Document {
self._implementation = null;
self._ready_state = .loading;
self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame);
self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame, .{ .allow_declarative_shadow = true });
try self._script_created_parser.?.start();
frame._parse_mode = .document;

View File

@@ -153,18 +153,13 @@ pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Fra
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, false, frame);
}
frame.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
}
if (html.len == 0) {
return;
}
try frame.parseHtmlAsChildren(parent, html);
/// allows declarative shadow dom
pub fn setHTMLUnsafe(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, true, frame);
}
pub fn cloneFragment(self: *DocumentFragment, deep: bool, frame: *Frame) !*Node {

View File

@@ -484,18 +484,13 @@ pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void
pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, false, frame);
}
frame.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
}
if (html.len == 0) {
return;
}
try frame.parseHtmlAsChildren(parent, html);
/// allows declarative shadow dom
pub fn setHTMLUnsafe(self: *Element, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, true, frame);
}
pub fn getId(self: *const Element) []const u8 {
@@ -726,11 +721,41 @@ pub fn getAssignedSlot(self: *Element, frame: *Frame) ?*Html.Slot {
return frame._element_assigned_slots.get(self);
}
pub fn attachShadow(self: *Element, mode_str: []const u8, frame: *Frame) !*ShadowRoot {
if (frame._element_shadow_roots.get(self)) |_| {
return error.AlreadyHasShadowRoot;
// Whether this element may host a shadow root
fn isValidShadowHost(self: *const Element) bool {
if (self._namespace != .html) {
return false;
}
const mode = try ShadowRoot.Mode.fromString(mode_str);
return switch (self.getTag()) {
.article, .aside, .blockquote, .body, .div, .footer, .header, .main, .nav, .p, .section, .span, .h1, .h2, .h3, .h4, .h5, .h6, .custom => true,
else => false,
};
}
pub fn attachShadow(self: *Element, mode_str: String, frame: *Frame) !*ShadowRoot {
if (frame._element_shadow_roots.get(self)) |_| {
return error.NotSupported;
}
if (!self.isValidShadowHost()) {
return error.NotSupported;
}
// A custom element whose definition lists "shadow" in disabledFeatures
// cannot host a shadow root (imperative or declarative).
if (self.is(Html.Custom)) |custom| {
if (frame.window._custom_elements._definitions.get(custom._tag_name.str())) |def| {
if (def.disable_shadow) {
return error.NotSupported;
}
}
}
const mode: ShadowRoot.Mode = blk: {
if (mode_str.eql(comptime .wrap("open"))) break :blk .open;
if (mode_str.eql(comptime .wrap("closed"))) break :blk .closed;
return error.InvalidArgument;
};
const shadow_root = try ShadowRoot.init(self, mode, frame);
try frame._element_shadow_roots.put(frame.arena, self, shadow_root);
return shadow_root;
@@ -1792,11 +1817,12 @@ pub const JsApi = struct {
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true });
const ShadowRootInit = struct {
mode: []const u8,
mode: String,
};
fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot {
return self.attachShadow(init.mode, frame);

View File

@@ -1076,6 +1076,25 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v
}
}
/// Shared implementation in Element and DocumentFragment
pub fn setHTML(self: *Node, html: []const u8, allow_declarative_shadow: bool, frame: *Frame) !void {
frame.domChanged();
var it = self.childrenIterator();
while (it.next()) |child| {
frame.removeNode(self, child, .{ .will_be_reconnected = false });
}
if (html.len == 0) {
return;
}
if (allow_declarative_shadow) {
try frame.parseHtmlUnsafeAsChildren(self, html);
} else {
try frame.parseHtmlAsChildren(self, html);
}
}
// Writes a JSON representation of the node and its children
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
// stupid json api requires this to be const,

View File

@@ -29,10 +29,6 @@ const ShadowRoot = @This();
pub const Mode = enum {
open,
closed,
pub fn fromString(str: []const u8) !Mode {
return std.meta.stringToEnum(Mode, str) orelse error.InvalidMode;
}
};
_proto: *DocumentFragment,
@@ -70,6 +66,10 @@ pub fn getHost(self: *const ShadowRoot) *Element {
return self._host;
}
pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void {
return self.asDocumentFragment().setHTMLUnsafe(html, frame);
}
pub fn getElementById(self: *ShadowRoot, id: []const u8, frame: *Frame) ?*Element {
if (id.len == 0) {
return null;
@@ -137,6 +137,7 @@ pub const JsApi = struct {
return self.getElementById(try value.toZig([]const u8), frame);
}
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
};
const testing = @import("../../testing.zig");

View File

@@ -52,6 +52,8 @@ pub extern "C" fn html5ever_parse_document(
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
@@ -78,6 +80,8 @@ pub extern "C" fn html5ever_parse_document(
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
@@ -111,6 +115,8 @@ pub extern "C" fn html5ever_parse_document_with_encoding(
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
@@ -148,6 +154,8 @@ pub extern "C" fn html5ever_parse_document_with_encoding(
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
// Parse directly from decoded string
@@ -472,6 +480,8 @@ pub extern "C" fn html5ever_parse_fragment(
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
@@ -498,6 +508,8 @@ pub extern "C" fn html5ever_parse_fragment(
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
@@ -587,6 +599,8 @@ pub extern "C" fn html5ever_streaming_parser_create(
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> *mut c_void {
let arena = Box::new(typed_arena::Arena::new());
@@ -615,6 +629,8 @@ pub extern "C" fn html5ever_streaming_parser_create(
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
// Create a parser which implements TendrilSink for streaming parsing
@@ -716,6 +732,8 @@ pub extern "C" fn xml5ever_parse_document(
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if xml.is_null() || len == 0 {
return ();
@@ -742,6 +760,8 @@ pub extern "C" fn xml5ever_parse_document(
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(xml, len) };

View File

@@ -62,6 +62,8 @@ pub struct Sink<'arena> {
pub reparent_children_callback: ReparentChildrenCallback,
pub append_before_sibling_callback: AppendBeforeSiblingCallback,
pub append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
pub attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
pub allow_declarative_shadow: bool,
}
impl<'arena> TreeSink for Sink<'arena> {
@@ -286,4 +288,26 @@ impl<'arena> TreeSink for Sink<'arena> {
(self.reparent_children_callback)(self.ctx, *node, *new_parent);
}
}
fn allow_declarative_shadow_roots(&self, _intended_parent: &Ref) -> bool {
self.allow_declarative_shadow
}
fn attach_declarative_shadow(&self, location: &Ref, template: &Ref, attrs: &[Attribute]) -> bool {
// html5ever only calls this when shadowrootmode is "open" or "closed",
// so anything other than "open" is treated as "closed".
let mode_is_open = attrs
.iter()
.find(|a| a.name.local.as_ref() == "shadowrootmode")
.map(|a| a.value.as_ref() == "open")
.unwrap_or(true);
unsafe {
(self.attach_declarative_shadow_callback)(
self.ctx,
*location,
*template,
if mode_is_open { 1 } else { 0 },
) != 0
}
}
}

View File

@@ -65,6 +65,13 @@ pub type AddAttrsIfMissingCallback = unsafe extern "C" fn(
pub type GetTemplateContentsCallback = unsafe extern "C" fn(ctx: Ref, target: Ref) -> Ref;
pub type AttachDeclarativeShadowCallback = unsafe extern "C" fn(
ctx: Ref,
host: Ref,
template: Ref,
mode_is_open: u8,
) -> u8;
pub type RemoveFromParentCallback = unsafe extern "C" fn(ctx: Ref, target: Ref) -> ();
pub type ReparentChildrenCallback = unsafe extern "C" fn(ctx: Ref, node: Ref, new_parent: Ref) -> ();