mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-15 07:19:20 -04:00
Merge pull request #3181 from lightpanda-io/getHTML
webapi: Element/ShadowRoot getHTML
This commit is contained in:
5 files changed
+289
-18
No files matched your search
+82
-13
@@ -36,7 +36,7 @@ pub const Opts = struct {
|
||||
invisible: bool = false,
|
||||
};
|
||||
|
||||
pub const Shadow = enum {
|
||||
pub const Shadow = union(enum) {
|
||||
// Skip shadow DOM entirely (innerHTML/outerHTML)
|
||||
skip,
|
||||
|
||||
@@ -45,6 +45,16 @@ pub const Opts = struct {
|
||||
|
||||
// Resolve slot elements (like what actually gets rendered)
|
||||
rendered,
|
||||
|
||||
// Element/ShadowRoot.getHTML can control how it handles shadow elements
|
||||
declarative: Declarative,
|
||||
|
||||
pub const Declarative = struct {
|
||||
// Serialize shadow roots whose `serializable` flag is set
|
||||
serializable_shadow_roots: bool = false,
|
||||
// Serialize these roots regardless of their flags or mode
|
||||
shadow_roots: []const *Node.ShadowRoot = &.{},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -125,20 +135,30 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
||||
return writer.writeAll("</slot>");
|
||||
}
|
||||
}
|
||||
if (opts.shadow != .skip) {
|
||||
if (frame._element_shadow_roots.get(el)) |shadow| {
|
||||
try children(shadow.asNode(), opts, writer, frame);
|
||||
// In rendered mode, light DOM is only shown through slots, not directly
|
||||
if (opts.shadow == .rendered) {
|
||||
// Skip rendering light DOM children
|
||||
if (!isVoidElement(el)) {
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(el.getTagNameDump());
|
||||
try writer.writeByte('>');
|
||||
switch (opts.shadow) {
|
||||
.skip => {},
|
||||
.complete, .rendered => {
|
||||
if (frame._element_shadow_roots.get(el)) |shadow| {
|
||||
try children(shadow.asNode(), opts, writer, frame);
|
||||
// In rendered mode, light DOM is only shown through slots, not directly
|
||||
if (opts.shadow == .rendered) {
|
||||
// Skip rendering light DOM children
|
||||
if (!isVoidElement(el)) {
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(el.getTagNameDump());
|
||||
try writer.writeByte('>');
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
.declarative => |declarative| {
|
||||
if (frame._element_shadow_roots.get(el)) |shadow| {
|
||||
if (shouldSerializeShadow(shadow, declarative)) {
|
||||
try writeDeclarativeShadow(shadow, opts, writer, frame);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (opts.with_frames and el.is(IFrame) != null) {
|
||||
@@ -199,6 +219,22 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
||||
}
|
||||
}
|
||||
|
||||
// Element.getHTML / ShadowRoot.getHTML
|
||||
pub fn getHTML(node: *Node, declarative: Opts.Shadow.Declarative, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
const opts = Opts{ .shadow = .{ .declarative = declarative } };
|
||||
if (node.is(Node.Element)) |el| {
|
||||
if (frame._element_shadow_roots.get(el)) |shadow| {
|
||||
if (shouldSerializeShadow(shadow, declarative)) {
|
||||
// if the element's shadowroot tree is rendered before its
|
||||
// children (assume the opts say that it should serialize the
|
||||
// shadowroot at all (i.e. shouldSerializeShadow).
|
||||
try writeDeclarativeShadow(shadow, opts, writer, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
return children(node, opts, writer, frame);
|
||||
}
|
||||
|
||||
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
@@ -247,6 +283,39 @@ pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
|
||||
try writer.endObject();
|
||||
}
|
||||
|
||||
fn shouldSerializeShadow(shadow: *const Node.ShadowRoot, declarative: Opts.Shadow.Declarative) bool {
|
||||
if (declarative.serializable_shadow_roots and shadow._serializable) {
|
||||
return true;
|
||||
}
|
||||
for (declarative.shadow_roots) |sr| {
|
||||
if (sr == shadow) {
|
||||
// if it's explictly requested, it's serialized even if _serialized == false
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The spec's "attach a declarative shadow root" serialization: attribute order
|
||||
// is fixed, and boolean attributes serialize with an explicit ="".
|
||||
fn writeDeclarativeShadow(shadow: *Node.ShadowRoot, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
try writer.writeAll("<template shadowrootmode=\"");
|
||||
try writer.writeAll(@tagName(shadow._mode));
|
||||
try writer.writeByte('"');
|
||||
if (shadow._delegates_focus) {
|
||||
try writer.writeAll(" shadowrootdelegatesfocus=\"\"");
|
||||
}
|
||||
if (shadow._serializable) {
|
||||
try writer.writeAll(" shadowrootserializable=\"\"");
|
||||
}
|
||||
if (shadow._clonable) {
|
||||
try writer.writeAll(" shadowrootclonable=\"\"");
|
||||
}
|
||||
try writer.writeByte('>');
|
||||
try children(shadow.asNode(), opts, writer, frame);
|
||||
try writer.writeAll("</template>");
|
||||
}
|
||||
|
||||
fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
const assigned = slot.assignedNodes(null, frame) catch return;
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<body></body>
|
||||
|
||||
<script id="matches_inner_html_without_options">
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<span id="a">one</span><p class="x">two & three</p>';
|
||||
document.body.appendChild(div);
|
||||
|
||||
testing.expectEqual(div.innerHTML, div.getHTML());
|
||||
testing.expectEqual(div.innerHTML, div.getHTML({ serializableShadowRoots: false }));
|
||||
// A shadow host without qualification also matches innerHTML.
|
||||
const host = document.createElement('div');
|
||||
host.attachShadow({ mode: 'open' }).innerHTML = '<b>hidden</b>';
|
||||
host.innerHTML = '<i>light</i>';
|
||||
div.appendChild(host);
|
||||
testing.expectEqual(div.innerHTML, div.getHTML());
|
||||
testing.expectEqual(div.innerHTML, div.getHTML({ serializableShadowRoots: true }));
|
||||
|
||||
div.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="serializable_shadow_roots">
|
||||
{
|
||||
const wrapper = document.createElement('div');
|
||||
const host = document.createElement('div');
|
||||
wrapper.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open', serializable: true });
|
||||
shadow.innerHTML = '<slot></slot>';
|
||||
host.innerHTML = '<span>light</span>';
|
||||
|
||||
testing.expectEqual(
|
||||
'<div><template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span></div>',
|
||||
wrapper.getHTML({ serializableShadowRoots: true }));
|
||||
// default is false
|
||||
testing.expectEqual('<div><span>light</span></div>', wrapper.getHTML());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="flag_attributes_and_order">
|
||||
{
|
||||
const wrapper = document.createElement('div');
|
||||
const host = document.createElement('section');
|
||||
wrapper.appendChild(host);
|
||||
const shadow = host.attachShadow({
|
||||
mode: 'closed',
|
||||
serializable: true,
|
||||
clonable: true,
|
||||
delegatesFocus: true,
|
||||
});
|
||||
shadow.appendChild(document.createElement('slot'));
|
||||
|
||||
testing.expectEqual(
|
||||
'<section><template shadowrootmode="closed" shadowrootdelegatesfocus="" shadowrootserializable="" shadowrootclonable=""><slot></slot></template></section>',
|
||||
wrapper.getHTML({ serializableShadowRoots: true }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="own_shadow_root_serializes_first">
|
||||
{
|
||||
// getHTML called on the host itself: its own template precedes its light
|
||||
// children.
|
||||
const host = document.createElement('article');
|
||||
const shadow = host.attachShadow({ mode: 'open', serializable: true });
|
||||
shadow.innerHTML = '<slot></slot>';
|
||||
host.innerHTML = '<span>light</span>';
|
||||
|
||||
testing.expectEqual(
|
||||
'<template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span>',
|
||||
host.getHTML({ serializableShadowRoots: true }));
|
||||
testing.expectEqual(
|
||||
'<template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span>',
|
||||
host.getHTML({ shadowRoots: [shadow] }));
|
||||
// ...but not when it doesn't qualify.
|
||||
testing.expectEqual('<span>light</span>', host.getHTML());
|
||||
testing.expectEqual(host.innerHTML, host.getHTML());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="explicit_shadow_roots_list">
|
||||
{
|
||||
const wrapper = document.createElement('div');
|
||||
const host = document.createElement('div');
|
||||
wrapper.appendChild(host);
|
||||
// Not serializable, closed — only reachable via the explicit list.
|
||||
const shadow = host.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = '<em>secret</em>';
|
||||
|
||||
testing.expectEqual('<div></div>', wrapper.getHTML({ serializableShadowRoots: true }));
|
||||
const expected = '<div><template shadowrootmode="closed"><em>secret</em></template></div>';
|
||||
testing.expectEqual(expected, wrapper.getHTML({ shadowRoots: [shadow] }));
|
||||
testing.expectEqual(expected, wrapper.getHTML({ serializableShadowRoots: false, shadowRoots: [shadow] }));
|
||||
testing.expectEqual(expected, wrapper.getHTML({ serializableShadowRoots: true, shadowRoots: [shadow] }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="shadow_root_get_html">
|
||||
{
|
||||
// ShadowRoot.getHTML serializes the shadow root's own children.
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const outer = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const inner_host = document.createElement('div');
|
||||
outer.appendChild(inner_host);
|
||||
const inner = inner_host.attachShadow({ mode: 'open', serializable: true });
|
||||
inner.innerHTML = '<slot></slot>';
|
||||
inner_host.innerHTML = '<span>light</span>';
|
||||
|
||||
testing.expectEqual(
|
||||
'<div><template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span></div>',
|
||||
outer.getHTML({ serializableShadowRoots: true }));
|
||||
testing.expectEqual(outer.innerHTML, outer.getHTML());
|
||||
// The flag attribute reflects the root's serializable state even when it
|
||||
// qualified via the explicit list.
|
||||
testing.expectEqual(
|
||||
'<div><template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span></div>',
|
||||
outer.getHTML({ shadowRoots: [inner] }));
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="nested_serializable_roots">
|
||||
{
|
||||
const wrapper = document.createElement('div');
|
||||
const host = document.createElement('div');
|
||||
wrapper.appendChild(host);
|
||||
const outer = host.attachShadow({ mode: 'open', serializable: true });
|
||||
|
||||
const inner_host = document.createElement('p');
|
||||
outer.appendChild(inner_host);
|
||||
const inner = inner_host.attachShadow({ mode: 'open', serializable: true });
|
||||
inner.innerHTML = '<b>deep</b>';
|
||||
|
||||
testing.expectEqual(
|
||||
'<div><template shadowrootmode="open" shadowrootserializable=""><p><template shadowrootmode="open" shadowrootserializable=""><b>deep</b></template></p></template></div>',
|
||||
wrapper.getHTML({ serializableShadowRoots: true }));
|
||||
|
||||
// The explicit list does not descend for non-listed nested roots.
|
||||
testing.expectEqual(
|
||||
'<div><template shadowrootmode="open" shadowrootserializable=""><p></p></template></div>',
|
||||
wrapper.getHTML({ shadowRoots: [outer] }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="declarative_root_round_trip">
|
||||
{
|
||||
const wrapper = document.createElement('div');
|
||||
document.body.appendChild(wrapper);
|
||||
const html = '<div><template shadowrootmode="open" shadowrootserializable=""><slot></slot></template><span>light</span></div>';
|
||||
wrapper.setHTMLUnsafe(html);
|
||||
|
||||
testing.expectEqual(html, wrapper.getHTML({ serializableShadowRoots: true }));
|
||||
testing.expectEqual('<div><span>light</span></div>', wrapper.getHTML());
|
||||
|
||||
wrapper.remove();
|
||||
}
|
||||
</script>
|
||||
@@ -154,7 +154,10 @@ pub fn moveBefore(self: *DocumentFragment, node: js.Value, child: js.Value, fram
|
||||
|
||||
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
const dump = @import("../dump.zig");
|
||||
return dump.children(self.asNode(), .{ .shadow = .complete }, writer, frame);
|
||||
// Fragment serialization never includes nested shadow trees — a host
|
||||
// inside this fragment (or shadow root) serializes only its light
|
||||
// children, exactly like Element.innerHTML.
|
||||
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, frame);
|
||||
}
|
||||
|
||||
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -20,10 +20,11 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const dump = @import("../dump.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const reflect = @import("../reflect.zig");
|
||||
const Factory = @import("../Factory.zig");
|
||||
const StyleManager = @import("../StyleManager.zig");
|
||||
const reflect = @import("../reflect.zig");
|
||||
|
||||
const CSS = @import("CSS.zig");
|
||||
const Node = @import("Node.zig");
|
||||
@@ -518,7 +519,6 @@ pub fn insertAdjacentHTML(
|
||||
}
|
||||
|
||||
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
const dump = @import("../dump.zig");
|
||||
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, frame);
|
||||
}
|
||||
|
||||
@@ -582,10 +582,13 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
const dump = @import("../dump.zig");
|
||||
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, frame);
|
||||
}
|
||||
|
||||
pub fn getHTML(self: *Element, opts: dump.Opts.Shadow.Declarative, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
return dump.getHTML(self.asNode(), opts, writer, frame);
|
||||
}
|
||||
|
||||
pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asNode();
|
||||
return parent.setHTML(html, false, frame);
|
||||
@@ -2329,6 +2332,21 @@ pub const JsApi = struct {
|
||||
return self.setInnerHTML(if (value.isNull()) "" else try value.toZig([]const u8), frame);
|
||||
}
|
||||
|
||||
pub const getHTML = bridge.function(_getHTML, .{});
|
||||
const GetHTMLOpts = struct {
|
||||
serializableShadowRoots: bool = false,
|
||||
shadowRoots: []const *ShadowRoot = &.{},
|
||||
};
|
||||
fn _getHTML(self: *Element, opts_: ?GetHTMLOpts, frame: *Frame) ![]const u8 {
|
||||
const opts = opts_ orelse GetHTMLOpts{};
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
try self.getHTML(.{
|
||||
.shadow_roots = opts.shadowRoots,
|
||||
.serializable_shadow_roots = opts.serializableShadowRoots,
|
||||
}, &buf.writer, frame);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub const prefix = bridge.accessor(Element._prefix, null, .{});
|
||||
|
||||
pub const setAttribute = bridge.function(_setAttribute, .{ .ce_reactions = true });
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const dump = @import("../dump.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const DocumentFragment = @import("DocumentFragment.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const DocumentFragment = @import("DocumentFragment.zig");
|
||||
|
||||
const ShadowRoot = @This();
|
||||
|
||||
@@ -112,6 +114,10 @@ pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void {
|
||||
return self.asDocumentFragment().setHTMLUnsafe(html, frame);
|
||||
}
|
||||
|
||||
pub fn getHTML(self: *ShadowRoot, opts: dump.Opts.Shadow.Declarative, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
return dump.getHTML(self.asNode(), opts, writer, frame);
|
||||
}
|
||||
|
||||
pub fn getOnSlotChange(self: *ShadowRoot, frame: *Frame) ?js.Function.Global {
|
||||
return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onslotchange });
|
||||
}
|
||||
@@ -214,6 +220,20 @@ pub const JsApi = struct {
|
||||
}
|
||||
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .ce_reactions = true });
|
||||
pub const getHTML = bridge.function(_getHTML, .{});
|
||||
const GetHTMLOpts = struct {
|
||||
serializableShadowRoots: bool = false,
|
||||
shadowRoots: []const *ShadowRoot = &.{},
|
||||
};
|
||||
fn _getHTML(self: *ShadowRoot, opts_: ?GetHTMLOpts, frame: *Frame) ![]const u8 {
|
||||
const opts = opts_ orelse GetHTMLOpts{};
|
||||
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
|
||||
try self.getHTML(.{
|
||||
.shadow_roots = opts.shadowRoots,
|
||||
.serializable_shadow_roots = opts.serializableShadowRoots,
|
||||
}, &buf.writer, frame);
|
||||
return buf.written();
|
||||
}
|
||||
pub const onslotchange = bridge.accessor(ShadowRoot.getOnSlotChange, ShadowRoot.setOnSlotChange, .{});
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user