Skip to content

lit compatibility #928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 11, 2025
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
2 changes: 2 additions & 0 deletions src/browser/State.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const parser = @import("netsurf.zig");
const DataSet = @import("html/DataSet.zig");
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
const StyleSheet = @import("cssom/StyleSheet.zig");
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");

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

// for dom/document
active_element: ?*parser.Element = null,
adopted_style_sheets: ?Env.JsObject = null,

// for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should
Expand Down
30 changes: 29 additions & 1 deletion src/browser/cssom/CSSStyleSheet.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

const std = @import("std");

const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const StyleSheet = @import("StyleSheet.zig");
const CSSRuleList = @import("CSSRuleList.zig");
Expand All @@ -39,7 +40,7 @@ const CSSStyleSheetOpts = struct {
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
const opts = _opts orelse CSSStyleSheetOpts{};
return .{
.proto = StyleSheet{ .disabled = opts.disabled },
.proto = .{ .disabled = opts.disabled },
.css_rules = .constructor(),
.owner_rule = null,
};
Expand Down Expand Up @@ -72,6 +73,24 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
_ = self.css_rules.list.orderedRemove(index);
}

pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
_ = self;
_ = text;
// TODO: clear self.css_rules
// parse text and re-populate self.css_rules

const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
}

pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
_ = self;
_ = text;
// TODO: clear self.css_rules
// parse text and re-populate self.css_rules
}

const testing = @import("../../testing.zig");
test "Browser.CSS.StyleSheet" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
Expand All @@ -85,5 +104,14 @@ test "Browser.CSS.StyleSheet" {
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
.{ "index1", "0" },
.{ "css.cssRules.length", "1" },

.{
\\ let replaced = false;
\\ css.replace('body{}').then(() => replaced = true);
,
null,
},
// microtasks are run between each statement
.{ "replaced", "true" },
}, .{});
}
23 changes: 23 additions & 0 deletions src/browser/dom/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,22 @@ pub const Document = struct {
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
return &.{};
}

pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
if (state.adopted_style_sheets) |obj| {
return obj;
}

const obj = try page.main_context.newArray(0).persist();
state.adopted_style_sheets = obj;
return obj;
}

pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
state.adopted_style_sheets = try sheets.persist();
}
};

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

try runner.testCases(&.{
.{ "const acss = document.adoptedStyleSheets", null },
.{ "acss.length", "0" },
.{ "acss.push(new CSSStyleSheet())", null },
.{ "document.adoptedStyleSheets.length", "1" },
}, .{});

const Case = testing.JsRunner.Case;
const tags = comptime parser.Tag.all();
var createElements: [(tags.len) * 2]Case = undefined;
Expand Down
34 changes: 29 additions & 5 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,18 @@ pub const Element = struct {
}

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

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

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

const fragment_node = parser.documentFragmentToNode(fragment);

// I'm not sure what the exact behavior is supposed to be. Initially,
// we were only copying the body of the document fragment. But it seems
// like head elements should be copied too. Specifically, some sites
Expand All @@ -166,9 +168,32 @@ pub const Element = struct {
// or an actual document. In a blank page, something like:
// x.innerHTML = '<script></script>';
// does _not_ create an empty script, but in a real page, it does. Weird.
const fragment_node = parser.documentFragmentToNode(fragment);
const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = try parser.nodeFirstChild(html) orelse return;
const body = try parser.nodeNextSibling(head) orelse return;

if (try parser.elementTag(self) == .template) {
// HTMLElementTemplate is special. We don't append these as children
// of the template, but instead set its content as the body of the
// fragment. Simpler to do this by copying the body children into
// a new fragment
const clean = try parser.documentCreateDocumentFragment(doc);
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(@alignCast(@ptrCast(clean)), child);
}

const state = try page.getOrCreateNodeState(node);
state.template_content = clean;
return;
}

// For any node other than a template, we copy the head and body elements
// as child nodes of the element
{
// First, copy some of the head element
const children = try parser.nodeGetChildNodes(head);
Expand All @@ -182,7 +207,6 @@ pub const Element = struct {
}

{
const body = try parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
Expand Down
25 changes: 25 additions & 0 deletions src/browser/dom/shadow_root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

const std = @import("std");
const parser = @import("../netsurf.zig");

const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;

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

pub const Mode = enum {
open,
Expand All @@ -38,6 +42,20 @@ pub const ShadowRoot = struct {
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
return Element.toInterface(self.host);
}

pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
if (self.adopted_style_sheets) |obj| {
return obj;
}

const obj = try page.main_context.newArray(0).persist();
self.adopted_style_sheets = obj;
return obj;
}

pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist();
}
};

const testing = @import("../../testing.zig");
Expand Down Expand Up @@ -80,4 +98,11 @@ test "Browser.DOM.ShadowRoot" {
.{ "sr2.append(n1)", null},
.{ "sr2.getElementById('conflict') == n1", "true" },
}, .{});

try runner.testCases(&.{
.{ "const acss = sr2.adoptedStyleSheets", null },
.{ "acss.length", "0" },
.{ "acss.push(new CSSStyleSheet())", null },
.{ "sr2.adoptedStyleSheets.length", "1" },
}, .{});
}
12 changes: 12 additions & 0 deletions src/browser/dump.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
const std = @import("std");

const parser = @import("netsurf.zig");
const Page = @import("page.zig").Page;
const Walker = @import("dom/walker.zig").WalkerChildren;

pub const Opts = struct {
// set to include element shadowroots in the dump
page: ?*const Page = null,

exclude_scripts: bool = false,
};

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

try writer.writeAll(">");

if (opts.page) |page| {
if (page.getNodeState(node)) |state| {
if (state.shadow_root) |sr| {
try writeChildren(@alignCast(@ptrCast(sr.proto)), opts, writer);
}
}
}

// void elements can't have any content.
if (try isVoid(parser.nodeToElement(node))) return;

Expand Down
6 changes: 6 additions & 0 deletions src/browser/html/elements.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,12 @@ test "Browser.HTML.HTMLTemplateElement" {
.{ "document.getElementById('abc')", "null" },
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
.{ "document.getElementById('abc').id", "abc" },
.{ "t.innerHTML = '<span>over</span><p>9000!</p>';", null },
.{ "t.content.childNodes.length", "2" },
.{ "t.content.childNodes[0].tagName", "SPAN" },
.{ "t.content.childNodes[0].innerHTML", "over" },
.{ "t.content.childNodes[1].tagName", "P" },
.{ "t.content.childNodes[1].innerHTML", "9000!" },
}, .{});
}

Expand Down
5 changes: 4 additions & 1 deletion src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ pub const Page = struct {
}

pub const DumpOpts = struct {
exclude_scripts: bool = false,
// set to include element shadowroots in the dump
page: ?*const Page = null,
with_base: bool = false,
exclude_scripts: bool = false,
};

// dump writes the page content into the given file.
Expand All @@ -162,6 +164,7 @@ pub const Page = struct {
}

try Dump.writeHTML(doc, .{
.page = opts.page,
.exclude_scripts = opts.exclude_scripts,
}, out);
}
Expand Down
3 changes: 2 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ fn run(alloc: Allocator) !void {
// dump
if (opts.dump) {
try page.dump(.{
.exclude_scripts = opts.noscript,
.page = page,
.with_base = opts.withbase,
.exclude_scripts = opts.noscript,
}, std.io.getStdOut());
}
},
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return promise;
}

pub fn newArray(self: *JsContext, len: u32) JsObject {
const arr = v8.Array.init(self.isolate, len);
return .{
.js_context = self,
.js_obj = arr.castTo(v8.Object),
};
}

// Wrap a v8.Exception
fn createException(self: *const JsContext, e: v8.Value) Exception {
return .{
Expand Down