Skip to content

Commit 452c1e8

Browse files
committed
lit compatibility
Aims to improve compatibility for the lit framework (e.g. what Reddit is using). 1 - Adds support for adoptedStyleSheets to the Document and ShadowRoot 2 - Adds mock support for replace and replaceSync to the CSSStyleSheet 3 - Optionally include shadowroot in dump 4 - Special-case setting innerHTML on a TemplateElement
1 parent fbd40a6 commit 452c1e8

File tree

9 files changed

+138
-7
lines changed

9 files changed

+138
-7
lines changed

src/browser/State.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const parser = @import("netsurf.zig");
3131
const DataSet = @import("html/DataSet.zig");
3232
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
3333
const StyleSheet = @import("cssom/StyleSheet.zig");
34+
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
3435
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
3536

3637
// for HTMLScript (but probably needs to be added to more)
@@ -53,6 +54,7 @@ style_sheet: ?*StyleSheet = null,
5354

5455
// for dom/document
5556
active_element: ?*parser.Element = null,
57+
adopted_style_sheets: ?Env.JsObject = null,
5658

5759
// for HTMLSelectElement
5860
// By default, if no option is explicitly selected, the first option should

src/browser/cssom/CSSStyleSheet.zig

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
const std = @import("std");
2020

21+
const Env = @import("../env.zig").Env;
2122
const Page = @import("../page.zig").Page;
2223
const StyleSheet = @import("StyleSheet.zig");
2324
const CSSRuleList = @import("CSSRuleList.zig");
@@ -39,7 +40,7 @@ const CSSStyleSheetOpts = struct {
3940
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
4041
const opts = _opts orelse CSSStyleSheetOpts{};
4142
return .{
42-
.proto = StyleSheet{ .disabled = opts.disabled },
43+
.proto = .{ .disabled = opts.disabled },
4344
.css_rules = .constructor(),
4445
.owner_rule = null,
4546
};
@@ -72,6 +73,24 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
7273
_ = self.css_rules.list.orderedRemove(index);
7374
}
7475

76+
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
77+
_ = self;
78+
_ = text;
79+
// TODO: clear self.css_rules
80+
// parse text and re-populate self.css_rules
81+
82+
const resolver = page.main_context.createPromiseResolver();
83+
try resolver.resolve({});
84+
return resolver.promise();
85+
}
86+
87+
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
88+
_ = self;
89+
_ = text;
90+
// TODO: clear self.css_rules
91+
// parse text and re-populate self.css_rules
92+
}
93+
7594
const testing = @import("../../testing.zig");
7695
test "Browser.CSS.StyleSheet" {
7796
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
@@ -85,5 +104,14 @@ test "Browser.CSS.StyleSheet" {
85104
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
86105
.{ "index1", "0" },
87106
.{ "css.cssRules.length", "1" },
107+
108+
.{
109+
\\ let replaced = false;
110+
\\ css.replace('body{}').then(() => replaced = true);
111+
,
112+
null,
113+
},
114+
// microtasks are run between each statement
115+
.{ "replaced", "true" },
88116
}, .{});
89117
}

src/browser/dom/document.zig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,22 @@ pub const Document = struct {
293293
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
294294
return &.{};
295295
}
296+
297+
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
298+
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
299+
if (state.adopted_style_sheets) |obj| {
300+
return obj;
301+
}
302+
303+
const obj = try page.main_context.newArray(0).persist();
304+
state.adopted_style_sheets = obj;
305+
return obj;
306+
}
307+
308+
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
309+
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
310+
state.adopted_style_sheets = try sheets.persist();
311+
}
296312
};
297313

298314
const testing = @import("../../testing.zig");
@@ -484,6 +500,13 @@ test "Browser.DOM.Document" {
484500
.{ "v.nodeName", "DIV" },
485501
}, .{});
486502

503+
try runner.testCases(&.{
504+
.{ "const acss = document.adoptedStyleSheets", null },
505+
.{ "acss.length", "0" },
506+
.{ "acss.push(new CSSStyleSheet())", null },
507+
.{ "document.adoptedStyleSheets.length", "1" },
508+
}, .{});
509+
487510
const Case = testing.JsRunner.Case;
488511
const tags = comptime parser.Tag.all();
489512
var createElements: [(tags.len) * 2]Case = undefined;

src/browser/dom/element.zig

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,18 @@ pub const Element = struct {
137137
}
138138

139139
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
140-
var buf = std.ArrayList(u8).init(page.arena);
140+
var buf = std.ArrayList(u8).init(page.call_arena);
141141
try dump.writeChildren(parser.elementToNode(self), .{}, buf.writer());
142142
return buf.items;
143143
}
144144

145145
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
146-
var buf = std.ArrayList(u8).init(page.arena);
146+
var buf = std.ArrayList(u8).init(page.call_arena);
147147
try dump.writeNode(parser.elementToNode(self), .{}, buf.writer());
148148
return buf.items;
149149
}
150150

151-
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
151+
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
152152
const node = parser.elementToNode(self);
153153
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
154154
// parse the fragment
@@ -157,6 +157,8 @@ pub const Element = struct {
157157
// remove existing children
158158
try Node.removeChildren(node);
159159

160+
const fragment_node = parser.documentFragmentToNode(fragment);
161+
160162
// I'm not sure what the exact behavior is supposed to be. Initially,
161163
// we were only copying the body of the document fragment. But it seems
162164
// like head elements should be copied too. Specifically, some sites
@@ -166,9 +168,32 @@ pub const Element = struct {
166168
// or an actual document. In a blank page, something like:
167169
// x.innerHTML = '<script></script>';
168170
// does _not_ create an empty script, but in a real page, it does. Weird.
169-
const fragment_node = parser.documentFragmentToNode(fragment);
170171
const html = try parser.nodeFirstChild(fragment_node) orelse return;
171172
const head = try parser.nodeFirstChild(html) orelse return;
173+
const body = try parser.nodeNextSibling(head) orelse return;
174+
175+
if (try parser.elementTag(self) == .template) {
176+
// HTMLElementTemplate is special. We don't append these as children
177+
// of the template, but instead set its content as the body of the
178+
// fragment. Simpler to do this by copying the body children into
179+
// a new fragment
180+
const clean = try parser.documentCreateDocumentFragment(doc);
181+
const children = try parser.nodeGetChildNodes(body);
182+
const ln = try parser.nodeListLength(children);
183+
for (0..ln) |_| {
184+
// always index 0, because nodeAppendChild moves the node out of
185+
// the nodeList and into the new tree
186+
const child = try parser.nodeListItem(children, 0) orelse continue;
187+
_ = try parser.nodeAppendChild(@alignCast(@ptrCast(clean)), child);
188+
}
189+
190+
const state = try page.getOrCreateNodeState(node);
191+
state.template_content = clean;
192+
return;
193+
}
194+
195+
// For any node other than a template, we copy the head and body elements
196+
// as child nodes of the element
172197
{
173198
// First, copy some of the head element
174199
const children = try parser.nodeGetChildNodes(head);
@@ -182,7 +207,6 @@ pub const Element = struct {
182207
}
183208

184209
{
185-
const body = try parser.nodeNextSibling(head) orelse return;
186210
const children = try parser.nodeGetChildNodes(body);
187211
const ln = try parser.nodeListLength(children);
188212
for (0..ln) |_| {

src/browser/dom/shadow_root.zig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
const std = @import("std");
2020
const parser = @import("../netsurf.zig");
21+
22+
const Env = @import("../env.zig").Env;
23+
const Page = @import("../page.zig").Page;
2124
const Element = @import("element.zig").Element;
2225
const ElementUnion = @import("element.zig").Union;
2326

@@ -29,6 +32,7 @@ pub const ShadowRoot = struct {
2932
mode: Mode,
3033
host: *parser.Element,
3134
proto: *parser.DocumentFragment,
35+
adopted_style_sheets: ?Env.JsObject = null,
3236

3337
pub const Mode = enum {
3438
open,
@@ -38,6 +42,20 @@ pub const ShadowRoot = struct {
3842
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
3943
return Element.toInterface(self.host);
4044
}
45+
46+
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
47+
if (self.adopted_style_sheets) |obj| {
48+
return obj;
49+
}
50+
51+
const obj = try page.main_context.newArray(0).persist();
52+
self.adopted_style_sheets = obj;
53+
return obj;
54+
}
55+
56+
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
57+
self.adopted_style_sheets = try sheets.persist();
58+
}
4159
};
4260

4361
const testing = @import("../../testing.zig");
@@ -70,4 +88,11 @@ test "Browser.DOM.ShadowRoot" {
7088
.{ "sr2.host == div2", "true" },
7189
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
7290
}, .{});
91+
92+
try runner.testCases(&.{
93+
.{ "const acss = sr2.adoptedStyleSheets", null },
94+
.{ "acss.length", "0" },
95+
.{ "acss.push(new CSSStyleSheet())", null },
96+
.{ "sr2.adoptedStyleSheets.length", "1" },
97+
}, .{});
7398
}

src/browser/dump.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
const std = @import("std");
2020

2121
const parser = @import("netsurf.zig");
22+
const Page = @import("page.zig").Page;
2223
const Walker = @import("dom/walker.zig").WalkerChildren;
2324

2425
pub const Opts = struct {
26+
// set to include element shadowroots in the dump
27+
page: ?*const Page = null,
28+
2529
exclude_scripts: bool = false,
2630
};
2731

@@ -88,6 +92,14 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void
8892

8993
try writer.writeAll(">");
9094

95+
if (opts.page) |page| {
96+
if (page.getNodeState(node)) |state| {
97+
if (state.shadow_root) |sr| {
98+
try writeChildren(@alignCast(@ptrCast(sr.proto)), opts, writer);
99+
}
100+
}
101+
}
102+
91103
// void elements can't have any content.
92104
if (try isVoid(parser.nodeToElement(node))) return;
93105

src/browser/html/elements.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,12 @@ test "Browser.HTML.HTMLTemplateElement" {
14541454
.{ "document.getElementById('abc')", "null" },
14551455
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
14561456
.{ "document.getElementById('abc').id", "abc" },
1457+
.{ "t.innerHTML = '<span>over</span><p>9000!</p>';", null },
1458+
.{ "t.content.childNodes.length", "2" },
1459+
.{ "t.content.childNodes[0].tagName", "SPAN" },
1460+
.{ "t.content.childNodes[0].innerHTML", "over" },
1461+
.{ "t.content.childNodes[1].tagName", "P" },
1462+
.{ "t.content.childNodes[1].innerHTML", "9000!" },
14571463
}, .{});
14581464
}
14591465

src/main.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ fn run(alloc: Allocator) !void {
134134

135135
// dump
136136
if (opts.dump) {
137-
try page.dump(.{ .exclude_scripts = opts.noscript }, std.io.getStdOut());
137+
try page.dump(.{
138+
.page = page,
139+
.exclude_scripts = opts.noscript,
140+
}, std.io.getStdOut());
138141
}
139142
},
140143
else => unreachable,

src/runtime/js.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
799799
return promise;
800800
}
801801

802+
pub fn newArray(self: *JsContext, len: u32) JsObject {
803+
const arr = v8.Array.init(self.isolate, len);
804+
return .{
805+
.js_context = self,
806+
.js_obj = arr.castTo(v8.Object),
807+
};
808+
}
809+
802810
// Wrap a v8.Exception
803811
fn createException(self: *const JsContext, e: v8.Value) Exception {
804812
return .{

0 commit comments

Comments
 (0)