Merge pull request #928 from lightpanda-io/lit_compat
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled

lit compatibility
This commit is contained in:
Karl Seguin
2025-08-11 08:30:34 +08:00
committed by GitHub
10 changed files with 140 additions and 8 deletions

View File

@@ -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)
@@ -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

View File

@@ -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");
@@ -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,
};
@@ -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, .{});
@@ -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" },
}, .{});
}

View File

@@ -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");
@@ -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;

View File

@@ -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
@@ -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
@@ -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);
@@ -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) |_| {

View File

@@ -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;
@@ -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,
@@ -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");
@@ -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" },
}, .{});
}

View File

@@ -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,
};
@@ -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;

View File

@@ -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!" },
}, .{});
}

View File

@@ -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.
@@ -162,6 +164,7 @@ pub const Page = struct {
}
try Dump.writeHTML(doc, .{
.page = opts.page,
.exclude_scripts = opts.exclude_scripts,
}, out);
}

View File

@@ -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());
}
},

View File

@@ -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 .{