Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,17 @@ pub const Element = struct {
state.shadow_root = sr;
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));

// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
// parent. When we render, we go top-down, so we'll have the element, get
// its shadowroot, and go on. that's what the above code does.
// But we sometimes need to go bottom-up, e.g when we have a slot element
// and want to find the containing parent. Unforatunately , we don't have
// that link, so we need to create it. In the DOM, the ShadowRoot is
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
// So we can also store the ShadowRoot in the DocumentFragment's state.
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
fragment_state.shadow_root = sr;

return sr;
}

Expand Down
17 changes: 15 additions & 2 deletions src/browser/dom/node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
const Walker = @import("walker.zig").WalkerDepthFirst;

// HTML
Expand Down Expand Up @@ -354,11 +355,23 @@ pub const Node = struct {
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
// - An Element inside a shadow DOM will return the associated ShadowRoot.
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
const GetRootNodeResult = union(enum) {
shadow_root: *ShadowRoot,
node: Union,
};
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
if (options) |options_| if (options_.composed) {
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
return try Node.toInterface(try parser.nodeGetRootNode(self));

const root = try parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
return .{ .shadow_root = sr };
}
}

return .{ .node = try Node.toInterface(root) };
}

pub fn _hasChildNodes(self: *parser.Node) !bool {
Expand Down
102 changes: 102 additions & 0 deletions src/browser/html/elements.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch;
const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
const NodeUnion = @import("../dom/node.zig").Union;
const Element = @import("../dom/element.zig").Element;
const DataSet = @import("DataSet.zig");

Expand Down Expand Up @@ -85,6 +86,7 @@ pub const Interfaces = .{
HTMLScriptElement,
HTMLSourceElement,
HTMLSpanElement,
HTMLSlotElement,
HTMLStyleElement,
HTMLTableElement,
HTMLTableCaptionElement,
Expand Down Expand Up @@ -1008,6 +1010,101 @@ pub const HTMLSpanElement = struct {
pub const subtype = .node;
};

pub const HTMLSlotElement = struct {
pub const Self = parser.Slot;
pub const prototype = *HTMLElement;
pub const subtype = .node;

pub fn get_name(self: *parser.Slot) !?[]const u8 {
return (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name")) orelse "";
}

pub fn set_name(self: *parser.Slot, value: []const u8) !void {
return parser.elementSetAttribute(@ptrCast(@alignCast(self)), "name", value);
}

const AssignedNodesOpts = struct {
flatten: bool = false,
};
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };

if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
return nodes;
}

if (!opts.flatten) {
return &.{};
}

const node: *parser.Node = @ptrCast(@alignCast(self));
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}

var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
assigned[i] = try Node.toInterface(child);
}
return assigned[0..i];
}

fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
if (opts.flatten) {
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
}

const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
const node: *parser.Node = @ptrCast(@alignCast(self));
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
}
}

var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try w.get_next(root, next) orelse break;
if (try parser.nodeType(next.?) != .element) {
if (slot_name == null) {
// default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
}
continue;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");

if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
}
}
return if (arr.items.len == 0) null else arr.items;
}

fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
if (a == null and b == null) {
return true;
}
if (a) |aa| {
const bb = b orelse return false;
return std.mem.eql(u8, aa, bb);
}

// a is null, but b isn't (else the first guard clause would have hit)
return false;
}
};

pub const HTMLStyleElement = struct {
pub const Self = parser.Style;
pub const prototype = *HTMLElement;
Expand Down Expand Up @@ -1168,6 +1265,7 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag)
.select => .{ .HTMLSelectElement = @as(*parser.Select, @ptrCast(e)) },
.source => .{ .HTMLSourceElement = @as(*parser.Source, @ptrCast(e)) },
.span => .{ .HTMLSpanElement = @as(*parser.Span, @ptrCast(e)) },
.slot => .{ .HTMLSlotElement = @as(*parser.Slot, @ptrCast(e)) },
.style => .{ .HTMLStyleElement = @as(*parser.Style, @ptrCast(e)) },
.table => .{ .HTMLTableElement = @as(*parser.Table, @ptrCast(e)) },
.caption => .{ .HTMLTableCaptionElement = @as(*parser.TableCaption, @ptrCast(e)) },
Expand Down Expand Up @@ -1484,6 +1582,10 @@ test "Browser: HTML.HTMLScriptElement" {
try testing.htmlRunner("html/script/inline_defer.html");
}

test "Browser: HTML.HTMLSlotElement" {
try testing.htmlRunner("html/html_slot_element.html");
}

const Check = struct {
input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected
Expand Down
2 changes: 2 additions & 0 deletions src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ pub const Tag = enum(u8) {
_var = c.DOM_HTML_ELEMENT_TYPE_VAR,
video = c.DOM_HTML_ELEMENT_TYPE_VIDEO,
wbr = c.DOM_HTML_ELEMENT_TYPE_WBR,
slot = c.DOM_HTML_ELEMENT_TYPE_SLOT,
undef = c.DOM_HTML_ELEMENT_TYPE__UNKNOWN,

pub fn all() []Tag {
Expand Down Expand Up @@ -1973,6 +1974,7 @@ pub const Script = c.dom_html_script_element;
pub const Select = c.dom_html_select_element;
pub const Source = struct { base: *c.dom_html_element };
pub const Span = struct { base: *c.dom_html_element };
pub const Slot = c.dom_html_slot_element;
pub const Style = c.dom_html_style_element;
pub const Table = c.dom_html_table_element;
pub const TableCaption = c.dom_html_table_caption_element;
Expand Down
66 changes: 66 additions & 0 deletions src/tests/html/html_slot_element.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script src="../testing.js"></script>
<script>
class LightPanda extends HTMLElement {
constructor() {
super();
}

connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });

const slot1 = document.createElement('slot');
slot1.name = 'slot-1';
shadow.appendChild(slot1);

switch (this.getAttribute('mode')) {
case '1':
slot1.innerHTML = 'hello';
break;
case '2':
const slot2 = document.createElement('slot');
shadow.appendChild(slot2);
break;
}
}
}
window.customElements.define("lp-test", LightPanda);
</script>

<lp-test id=lp1 mode=1></lp-test>
<lp-test id=lp2 mode=0></lp-test>
<lp-test id=lp3 mode=0>default</lp-test>
<lp-test id=lp4 mode=1><p slot=other>default</p></lp-test>
<lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test>
<lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test>

<script id=HTMLSlotElement>
function assertNodes(expected, actual) {
actual = actual.map((n) => n.id || n.textContent)
testing.expectEqual(expected, actual);
}

for (let idx of [1, 2, 3, 4]) {
const lp = $(`#lp${idx}`);
const slot = lp.shadowRoot.querySelector('slot');

assertNodes([], slot.assignedNodes());
assertNodes([], slot.assignedNodes({}));
assertNodes([], slot.assignedNodes({flatten: false}));
if (lp.getAttribute('mode') === '1') {
assertNodes(['hello'], slot.assignedNodes({flatten: true}));
} else {
assertNodes([], slot.assignedNodes({flatten: true}));
}
}

const lp5 = $('#lp5');
const s5 = lp5.shadowRoot.querySelector('slot');
assertNodes(['default', 'other'], s5.assignedNodes());

const lp6 = $('#lp6');
const s6 = lp6.shadowRoot.querySelectorAll('slot');
assertNodes(['default2'], s6[0].assignedNodes({}));
assertNodes(['default2'], s6[0].assignedNodes({flatten: true}));
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({}));
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true}));
</script>