mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge pull request #2556 from lightpanda-io/declarative_shadow_dom
Add declarative shadow dom (DSD)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
124
src/browser/tests/shadowroot/declarative.html
Normal file
124
src/browser/tests/shadowroot/declarative.html
Normal 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>
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) -> ();
|
||||
|
||||
Reference in New Issue
Block a user