Introduces an Env.String for persistent strings

If a webapi has a []const u8 parameter, then the page.call_arena is used. This
is our favorite arena to use, but if the string value has a lifetime beyond the
call, it then needs to be duped again (using page.arena).

When a webapi has a Env.String parameter, the page.arena will be used directly
to get the value from V8, removing the need for an intermediary dupe in the
call_arena.

This allows HTMLCollections to be streamlined. They no longer need to dupe the
filter (tag name, class name, ...), which means they can no longer fail. It also
means that we no longer need to needlessly dupe the string literals.
This commit is contained in:
Karl Seguin
2025-09-17 12:08:26 +08:00
parent b7d26cf0d5
commit 6b9dc90639
9 changed files with 127 additions and 127 deletions

View File

@@ -156,22 +156,14 @@ pub const Document = struct {
// the spec changed to return an HTMLCollection instead. // the spec changed to return an HTMLCollection instead.
// That's why we reimplemented getElementsByTagName by using an // That's why we reimplemented getElementsByTagName by using an
// HTMLCollection in zig here. // HTMLCollection in zig here.
pub fn _getElementsByTagName( pub fn _getElementsByTagName(self: *parser.Document, tag_name: Env.String) !collection.HTMLCollection {
self: *parser.Document, return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
.include_root = true, .include_root = true,
}); });
} }
pub fn _getElementsByClassName( pub fn _getElementsByClassName(self: *parser.Document, class_names: Env.String) !collection.HTMLCollection {
self: *parser.Document, return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
.include_root = true, .include_root = true,
}); });
} }

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const css = @import("css.zig"); const css = @import("css.zig");
@@ -350,28 +351,18 @@ pub const Element = struct {
return try parser.elementRemoveAttributeNode(self, attr); return try parser.elementRemoveAttributeNode(self, attr);
} }
pub fn _getElementsByTagName( pub fn _getElementsByTagName(self: *parser.Element, tag_name: Env.String) !collection.HTMLCollection {
self: *parser.Element, return collection.HTMLCollectionByTagName(
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
tag_name, tag_name.string,
.{ .include_root = false }, .{ .include_root = false },
); );
} }
pub fn _getElementsByClassName( pub fn _getElementsByClassName(self: *parser.Element, class_names: Env.String) !collection.HTMLCollection {
self: *parser.Element,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName( return try collection.HTMLCollectionByClassName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
classNames, class_names.string,
.{ .include_root = false }, .{ .include_root = false },
); );
} }

View File

@@ -52,13 +52,13 @@ pub const MatchByTagName = struct {
tag: []const u8, tag: []const u8,
is_wildcard: bool, is_wildcard: bool,
fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName { fn init(tag_name: []const u8) MatchByTagName {
if (std.mem.eql(u8, tag_name, "*")) { if (std.mem.eql(u8, tag_name, "*")) {
return .{ .tag = "*", .is_wildcard = true }; return .{ .tag = "*", .is_wildcard = true };
} }
return .{ return .{
.tag = try arena.dupe(u8, tag_name), .tag = tag_name,
.is_wildcard = false, .is_wildcard = false,
}; };
} }
@@ -69,15 +69,14 @@ pub const MatchByTagName = struct {
}; };
pub fn HTMLCollectionByTagName( pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
tag_name: []const u8, tag_name: []const u8,
opts: Opts, opts: Opts,
) !HTMLCollection { ) HTMLCollection {
return HTMLCollection{ return .{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) }, .matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -86,9 +85,9 @@ pub fn HTMLCollectionByTagName(
pub const MatchByClassName = struct { pub const MatchByClassName = struct {
class_names: []const u8, class_names: []const u8,
fn init(arena: Allocator, class_names: []const u8) !MatchByClassName { fn init(class_names: []const u8) !MatchByClassName {
return .{ return .{
.class_names = try arena.dupe(u8, class_names), .class_names = class_names,
}; };
} }
@@ -107,15 +106,14 @@ pub const MatchByClassName = struct {
}; };
pub fn HTMLCollectionByClassName( pub fn HTMLCollectionByClassName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
classNames: []const u8, class_names: []const u8,
opts: Opts, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) }, .matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -124,10 +122,8 @@ pub fn HTMLCollectionByClassName(
pub const MatchByName = struct { pub const MatchByName = struct {
name: []const u8, name: []const u8,
fn init(arena: Allocator, name: []const u8) !MatchByName { fn init(name: []const u8) !MatchByName {
return .{ return .{ .name = name };
.name = try arena.dupe(u8, name),
};
} }
pub fn match(self: MatchByName, node: *parser.Node) !bool { pub fn match(self: MatchByName, node: *parser.Node) !bool {
@@ -138,7 +134,6 @@ pub const MatchByName = struct {
}; };
pub fn HTMLCollectionByName( pub fn HTMLCollectionByName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
name: []const u8, name: []const u8,
opts: Opts, opts: Opts,
@@ -146,7 +141,7 @@ pub fn HTMLCollectionByName(
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(arena, name) }, .matcher = .{ .matchByName = try MatchByName.init(name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -203,8 +198,8 @@ pub fn HTMLCollectionChildren(
}; };
} }
pub fn HTMLCollectionEmpty() !HTMLCollection { pub fn HTMLCollectionEmpty() HTMLCollection {
return HTMLCollection{ return .{
.root = null, .root = null,
.walker = .{ .walkerNone = .{} }, .walker = .{ .walkerNone = .{} },
.matcher = .{ .matchFalse = .{} }, .matcher = .{ .matchFalse = .{} },
@@ -226,14 +221,11 @@ pub const MatchByLinks = struct {
} }
}; };
pub fn HTMLCollectionByLinks( pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection {
root: ?*parser.Node, return .{
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = MatchByLinks{} }, .matcher = .{ .matchByLinks = .{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -252,14 +244,11 @@ pub const MatchByAnchors = struct {
} }
}; };
pub fn HTMLCollectionByAnchors( pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection {
root: ?*parser.Node, return .{
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = MatchByAnchors{} }, .matcher = .{ .matchByAnchors = .{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
@@ -101,13 +102,20 @@ pub const NodeList = struct {
nodes: NodesArrayList = .{}, nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void { pub fn deinit(self: *NodeList, allocator: Allocator) void {
// TODO unref all nodes self.nodes.deinit(allocator);
self.nodes.deinit(alloc);
} }
pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void { pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void {
try self.nodes.append(alloc, node); return self.nodes.ensureTotalCapacity(allocator, n);
}
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
try self.nodes.append(allocator, node);
}
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
self.nodes.appendAssumeCapacity(node);
} }
pub fn get_length(self: *const NodeList) u32 { pub fn get_length(self: *const NodeList) u32 {

View File

@@ -61,7 +61,7 @@ pub const Performance = struct {
return milliTimestamp() - self.time_origin; return milliTimestamp() - self.time_origin;
} }
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { pub fn _mark(_: *Performance, name: Env.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page); const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list // TODO: Should store this in an entries list
return mark; return mark;
@@ -155,7 +155,7 @@ pub const PerformanceMark = struct {
startTime: ?f64 = null, startTime: ?f64 = null,
}; };
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark { pub fn constructor(name: Env.String, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance; const perf = &page.window.performance;
const options = _options orelse Options{}; const options = _options orelse Options{};
@@ -166,9 +166,7 @@ pub const PerformanceMark = struct {
} }
const detail = if (options.detail) |d| try d.persist() else null; const detail = if (options.detail) |d| try d.persist() else null;
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
const duped_name = try page.arena.dupe(u8, name);
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
return .{ .proto = proto, .detail = detail }; return .{ .proto = proto, .detail = detail };
} }

View File

@@ -115,67 +115,69 @@ pub const HTMLDocument = struct {
} }
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList { pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
const arena = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
if (name.len == 0) return list; if (name.len == 0) {
return list;
}
const root = parser.documentHTMLToNode(self); const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(arena, root, name, .{ var c = try collection.HTMLCollectionByName(root, name, .{
.include_root = false, .include_root = false,
}); });
const ln = try c.get_length(); const ln = try c.get_length();
try list.ensureTotalCapacity(page.arena, ln);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) : (i += 1) {
const n = try c.item(i) orelse break; const n = try c.item(i) orelse break;
try list.append(arena, n); list.appendAssumeCapacity(n);
i += 1;
} }
return list; return list;
} }
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_images(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{ return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "img", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_embeds(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{ return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "embed", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_plugins(self: *parser.DocumentHTML) collection.HTMLCollection {
return get_embeds(self, page); return get_embeds(self);
} }
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_forms(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{ return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "form", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_scripts(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{ return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "script", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_applets(_: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionEmpty(); return collection.HTMLCollectionEmpty();
} }
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_links(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{ return collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_anchors(self: *parser.DocumentHTML) collection.HTMLCollection {
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{ return collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }

View File

@@ -246,10 +246,10 @@ pub const Window = struct {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
} }
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList { pub fn _matchMedia(_: *const Window, media: Env.String) !MediaQueryList {
return .{ return .{
.matches = false, // TODO? .matches = false, // TODO?
.media = try page.arena.dupe(u8, media), .media = media.string,
}; };
} }

View File

@@ -1196,6 +1196,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return try self.createFunction(js_value); return try self.createFunction(js_value);
} }
if (T == String) {
return .{ .string = try valueToString(self.context_arena, js_value, self.isolate, self.v8_context) };
}
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
if (comptime isJsObject(T)) { if (comptime isJsObject(T)) {
@@ -2184,6 +2188,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
promise: v8.Promise, promise: v8.Promise,
}; };
// When doing jsValueToZig, string ([]const u8) are managed by the
// call_arena. That means that if the API wants to persist the string
// (which is relatively common), it needs to dupe it again.
// If the parameter is an Env.String rather than a []const u8, then
// the page's arena will be used (rather than the call arena).
pub const String = struct {
string: []const u8,
};
pub const Inspector = struct { pub const Inspector = struct {
isolate: v8.Isolate, isolate: v8.Isolate,
inner: *v8.Inspector, inner: *v8.Inspector,

View File

@@ -10,50 +10,57 @@
</body> </body>
<script src="../testing.js"></script> <script src="../testing.js"></script>
<script id=exceptions> <script id=caseInsensitve>
let content = $('#content'); const Ptags = document.getElementsByTagName('P');
let pe = $('#para-empty'); testing.expectEqual(2, Ptags.length);
testing.expectEqual('p', Ptags.item(0).localName);
testing.expectEqual('p', Ptags.item(1).localName);
</script>
let getElementsByTagName = document.getElementsByTagName('p'); <script id=all>
testing.expectEqual(2, getElementsByTagName.length); let allTags = document.getElementsByTagName('*');
testing.expectEqual(13, allTags.length);
testing.expectEqual('html', allTags.item(0).localName);
testing.expectEqual('html', allTags.item(0).localName);
testing.expectEqual('head', allTags.item(1).localName);
testing.expectEqual('html', allTags.item(0).localName);
testing.expectEqual('body', allTags.item(2).localName);
testing.expectEqual('div', allTags.item(3).localName);
testing.expectEqual('p', allTags.item(7).localName);
testing.expectEqual('span', allTags.namedItem('para-empty-child').localName);
let getElementsByTagNameCI = document.getElementsByTagName('P');
testing.expectEqual(2, getElementsByTagNameCI.length);
testing.expectEqual('p', getElementsByTagName.item(0).localName);
testing.expectEqual('p', getElementsByTagName.item(1).localName);
let getElementsByTagNameAll = document.getElementsByTagName('*');
testing.expectEqual(10, getElementsByTagNameAll.length);
testing.expectEqual('html', getElementsByTagNameAll.item(0).localName);
testing.expectEqual('html', getElementsByTagNameAll.item(0).localName);
testing.expectEqual('head', getElementsByTagNameAll.item(1).localName);
testing.expectEqual('html', getElementsByTagNameAll.item(0).localName);
testing.expectEqual('body', getElementsByTagNameAll.item(2).localName);
testing.expectEqual('div', getElementsByTagNameAll.item(3).localName);
testing.expectEqual('p', getElementsByTagNameAll.item(7).localName);
testing.expectEqual('span', getElementsByTagNameAll.namedItem('para-empty-child').localName);
// array like // array like
testing.expectEqual('html', getElementsByTagNameAll[0].localName); testing.expectEqual('html', allTags[0].localName);
testing.expectEqual('p', getElementsByTagNameAll[7].localName); testing.expectEqual('p', allTags[7].localName);
testing.expectEqual(undefined, getElementsByTagNameAll[11]); testing.expectEqual(undefined, allTags[14]);
testing.expectEqual('span', getElementsByTagNameAll['para-empty-child'].localName); testing.expectEqual('span', allTags['para-empty-child'].localName);
testing.expectEqual(undefined, getElementsByTagNameAll['foo']); testing.expectEqual(undefined, allTags['foo']);
</script>
<script id=element>
let content = $('#content');
testing.expectEqual(4, content.getElementsByTagName('*').length); testing.expectEqual(4, content.getElementsByTagName('*').length);
testing.expectEqual(2, content.getElementsByTagName('p').length); testing.expectEqual(2, content.getElementsByTagName('p').length);
testing.expectEqual(0, content.getElementsByTagName('div').length); testing.expectEqual(0, content.getElementsByTagName('div').length);
testing.expectEqual(1, document.children.length); testing.expectEqual(1, document.children.length);
testing.expectEqual(3, content.children.length); testing.expectEqual(3, content.children.length);
</script>
// check liveness
let p = document.createElement('p'); <script id=liveness>
testing.expectEqual('OK live', p.textContent = 'OK live'); const ptags = document.getElementsByTagName('p');
testing.expectEqual(' And', getElementsByTagName.item(1).textContent); testing.expectEqual(2, ptags.length);
testing.expectEqual(true, content.appendChild(p) != undefined); testing.expectEqual(' And', ptags.item(1).textContent);
testing.expectEqual(3, getElementsByTagName.length);
testing.expectEqual('OK live', getElementsByTagName.item(2).textContent); let p = document.createElement('p');
testing.expectEqual(true, content.insertBefore(p, pe) != undefined); p.textContent = 'OK live';
testing.expectEqual('OK live', getElementsByTagName.item(0).textContent); // hasn't been added, still 2
testing.expectEqual(2, ptags.length);
testing.expectEqual(true, content.appendChild(p) != undefined);
testing.expectEqual(3, ptags.length);
testing.expectEqual('OK live', ptags.item(2).textContent);
testing.expectEqual(true, content.insertBefore(p, $('#para-empty')) != undefined);
testing.expectEqual('OK live', ptags.item(0).textContent);
</script> </script>