Merge pull request #544 from lightpanda-io/token_list_iterators

Add missing TokenList APIs
This commit is contained in:
Pierre Tachoire
2025-04-22 09:03:19 +02:00
committed by GitHub
9 changed files with 394 additions and 42 deletions

View File

@@ -20,7 +20,7 @@ const DOMException = @import("exceptions.zig").DOMException;
const EventTarget = @import("event_target.zig").EventTarget;
const DOMImplementation = @import("implementation.zig").DOMImplementation;
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList;
const DOMTokenList = @import("token_list.zig");
const NodeList = @import("nodelist.zig");
const Node = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
@@ -30,7 +30,7 @@ pub const Interfaces = .{
EventTarget,
DOMImplementation,
NamedNodeMap,
DOMTokenList,
DOMTokenList.Interfaces,
NodeList.Interfaces,
Node.Node,
Node.Interfaces,

View File

@@ -24,7 +24,7 @@ const utils = @import("utils.z");
const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union;
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker;
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
@@ -443,15 +443,15 @@ pub const HTMLCollection = struct {
return null;
}
pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void {
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
const len = try self.get_length();
for (0..len) |i| {
const node = try self.item(@intCast(i)) orelse unreachable;
const e = @as(*parser.Element, @ptrCast(node));
try js_obj.setIndex(@intCast(i), e);
try js_this.setIndex(@intCast(i), e);
if (try item_name(e)) |name| {
try js_obj.set(name, e);
try js_this.set(name, e);
}
}
}

View File

@@ -22,7 +22,7 @@ const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env;
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
@@ -184,9 +184,9 @@ pub const MutationRecords = struct {
return null;
};
}
pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void {
pub fn postAttach(self: *const MutationRecords, js_this: JsThis) !void {
if (self.first) |mr| {
try js_obj.set("0", mr);
try js_this.set("0", mr);
}
}
};

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const parser = @import("../netsurf.zig");
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const Callback = @import("../env.zig").Callback;
const SessionState = @import("../env.zig").SessionState;
@@ -177,11 +177,11 @@ pub const NodeList = struct {
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_obj: JsObject) !void {
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
const len = self.get_length();
for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable;
try js_obj.setIndex(i, node);
try js_this.setIndex(i, node);
}
}
};

View File

@@ -19,9 +19,22 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig");
const Callback = @import("../env.zig").Callback;
const JsObject = @import("../env.zig").JsObject;
const SessionState = @import("../env.zig").SessionState;
const DOMException = @import("exceptions.zig").DOMException;
const log = std.log.scoped(.token_list);
pub const Interfaces = .{
DOMTokenList,
DOMTokenListIterable,
TokenListEntriesIterator,
TokenListEntriesIterator.Iterable,
};
// https://dom.spec.whatwg.org/#domtokenlist
pub const DOMTokenList = struct {
pub const Self = parser.TokenList;
@@ -98,7 +111,60 @@ pub const DOMTokenList = struct {
}
pub fn get_value(self: *parser.TokenList) !?[]const u8 {
return try parser.tokenListGetValue(self);
return (try parser.tokenListGetValue(self)) orelse "";
}
pub fn set_value(self: *parser.TokenList, value: []const u8) !void {
return parser.tokenListSetValue(self, value);
}
pub fn _toString(self: *parser.TokenList) ![]const u8 {
return (try get_value(self)) orelse "";
}
pub fn _keys(self: *parser.TokenList) !iterator.U32Iterator {
return .{ .length = try get_length(self) };
}
pub fn _values(self: *parser.TokenList) DOMTokenListIterable {
return DOMTokenListIterable.init(.{ .token_list = self });
}
pub fn _entries(self: *parser.TokenList) TokenListEntriesIterator {
return TokenListEntriesIterator.init(.{ .token_list = self });
}
pub fn _symbol_iterator(self: *parser.TokenList) DOMTokenListIterable {
return _values(self);
}
// TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: Callback, this_arg: JsObject) !void {
var entries = _entries(self);
while (try entries._next()) |entry| {
var result: Callback.Result = undefined;
cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.err("callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
};
}
}
};
const DOMTokenListIterable = iterator.Iterable(Iterator, "DOMTokenListIterable");
const TokenListEntriesIterator = iterator.NumericEntries(Iterator, "TokenListEntriesIterator");
pub const Iterator = struct {
index: u32 = 0,
token_list: *parser.TokenList,
// used when wrapped in an iterator.NumericEntries
pub const Error = parser.DOMError;
pub fn _next(self: *Iterator) !?[]const u8 {
const index = self.index;
self.index = index + 1;
return DOMTokenList._item(self.token_list, index);
}
};
@@ -150,4 +216,29 @@ test "Browser.DOM.TokenList" {
.{ "cl4.replace('nok', 'ok')", "true" },
.{ "cl4.value", "empty ok" },
}, .{});
try runner.testCases(&.{
.{ "let cl5 = gs.classList", "undefined" },
.{ "let keys = [...cl5.keys()]", "undefined" },
.{ "keys.length", "2" },
.{ "keys[0]", "0" },
.{ "keys[1]", "1" },
.{ "let values = [...cl5.values()]", "undefined" },
.{ "values.length", "2" },
.{ "values[0]", "empty" },
.{ "values[1]", "ok" },
.{ "let entries = [...cl5.entries()]", "undefined" },
.{ "entries.length", "2" },
.{ "entries[0]", "0,empty" },
.{ "entries[1]", "1,ok" },
}, .{});
try runner.testCases(&.{
.{ "let cl6 = gs.classList", "undefined" },
.{ "cl6.value = 'a b ccc'", "a b ccc" },
.{ "cl6.value", "a b ccc" },
.{ "cl6.toString()", "a b ccc" },
}, .{});
}

View File

@@ -21,6 +21,7 @@ const Interfaces = generate.Tuple(.{
@import("xmlserializer/xmlserializer.zig").Interfaces,
});
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Callback = Env.Callback;
pub const Env = js.Env(*SessionState, Interfaces{});

View File

@@ -28,24 +28,201 @@ pub const U32Iterator = struct {
.done = false,
};
}
// Iterators should be iterable. There's a [JS] example on MDN that
// suggests this is the correct approach:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
pub fn _symbol_iterator(self: *U32Iterator) *U32Iterator {
return self;
}
};
const testing = std.testing;
test "U32Iterator" {
const Return = U32Iterator.Return;
// A wrapper around an iterator that emits an Iterable result
// An iterable has a next() which emits a {done: bool, value: T} result
pub fn Iterable(comptime T: type, comptime JsName: []const u8) type {
// The inner iterator's return type.
// Maybe an error union.
// Definitely an optional
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
const CanError = @typeInfo(RawValue) == .error_union;
const Value = blk: {
// Unwrap the RawValue
var V = RawValue;
if (CanError) {
V = @typeInfo(V).error_union.payload;
}
break :blk @typeInfo(V).optional.child;
};
const Result = struct {
done: bool,
// todo, technically, we should return undefined when done = true
// or even omit the value;
value: ?Value,
};
const ReturnType = if (CanError) T.Error!Result else Result;
return struct {
// the inner value iterator
inner: T,
// Generics don't generate clean names. Can't just take the resulting
// type name and use that as a the JS class name. So we always ask for
// an explicit JS class name
pub const js_name = JsName;
const Self = @This();
pub fn init(inner: T) Self {
return .{ .inner = inner };
}
pub fn _next(self: *Self) ReturnType {
const value = if (comptime CanError) try self.inner._next() else self.inner._next();
return .{ .done = value == null, .value = value };
}
pub fn _symbol_iterator(self: *Self) *Self {
return self;
}
};
}
// A wrapper around an iterator that emits integer/index keyed entries.
pub fn NumericEntries(comptime T: type, comptime JsName: []const u8) type {
// The inner iterator's return type.
// Maybe an error union.
// Definitely an optional
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
const CanError = @typeInfo(RawValue) == .error_union;
const Value = blk: {
// Unwrap the RawValue
var V = RawValue;
if (CanError) {
V = @typeInfo(V).error_union.payload;
}
break :blk @typeInfo(V).optional.child;
};
const ReturnType = if (CanError) T.Error!?struct { u32, Value } else ?struct { u32, Value };
// Avoid ambiguity. We want to expose a NumericEntries(T).Iterable, so we
// need a declartion inside here for an "Iterable", but that will conflict
// with the above Iterable generic function we have.
const BaseIterable = Iterable;
return struct {
// the inner value iterator
inner: T,
index: u32,
const Self = @This();
// Generics don't generate clean names. Can't just take the resulting
// type name and use that as a the JS class name. So we always ask for
// an explicit JS class name
pub const js_name = JsName;
// re-exposed for when/if we compose this type into an Iterable
pub const Error = T.Error;
// This iterator as an iterable
pub const Iterable = BaseIterable(Self, JsName ++ "Iterable");
pub fn init(inner: T) Self {
return .{ .inner = inner, .index = 0 };
}
pub fn _next(self: *Self) ReturnType {
const value_ = if (comptime CanError) try self.inner._next() else self.inner._next();
const value = value_ orelse return null;
const index = self.index;
self.index = index + 1;
return .{ index, value };
}
// make the iterator, iterable
pub fn _symbol_iterator(self: *Self) Self.Iterable {
return Self.Iterable.init(self.*);
}
};
}
const testing = @import("../../testing.zig");
test "U32Iterator" {
{
var it = U32Iterator{ .length = 0 };
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
}
{
var it = U32Iterator{ .length = 3 };
try testing.expectEqual(Return{ .value = 0, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 1, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 2, .done = false }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
try testing.expectEqual(.{ .value = 0, .done = false }, it._next());
try testing.expectEqual(.{ .value = 1, .done = false }, it._next());
try testing.expectEqual(.{ .value = 2, .done = false }, it._next());
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
}
}
test "NumericEntries" {
const it = DummyIterator{};
var entries = NumericEntries(DummyIterator, "DummyIterator").init(it);
const v1 = entries._next().?;
try testing.expectEqual(0, v1.@"0");
try testing.expectEqual("it's", v1.@"1");
const v2 = entries._next().?;
try testing.expectEqual(1, v2.@"0");
try testing.expectEqual("over", v2.@"1");
const v3 = entries._next().?;
try testing.expectEqual(2, v3.@"0");
try testing.expectEqual("9000!!", v3.@"1");
try testing.expectEqual(null, entries._next());
try testing.expectEqual(null, entries._next());
try testing.expectEqual(null, entries._next());
}
test "Iterable" {
const it = DummyIterator{};
var entries = Iterable(DummyIterator, "DummyIterator").init(it);
const v1 = entries._next();
try testing.expectEqual(false, v1.done);
try testing.expectEqual("it's", v1.value.?);
const v2 = entries._next();
try testing.expectEqual(false, v2.done);
try testing.expectEqual("over", v2.value.?);
const v3 = entries._next();
try testing.expectEqual(false, v3.done);
try testing.expectEqual("9000!!", v3.value.?);
try testing.expectEqual(true, entries._next().done);
try testing.expectEqual(true, entries._next().done);
try testing.expectEqual(true, entries._next().done);
}
const DummyIterator = struct {
index: u32 = 0,
pub fn _next(self: *DummyIterator) ?[]const u8 {
const index = self.index;
self.index = index + 1;
return switch (index) {
0 => "it's",
1 => "over",
2 => "9000!!",
else => null,
};
}
};

View File

@@ -1697,6 +1697,11 @@ pub fn tokenListGetValue(l: *TokenList) !?[]const u8 {
return strToData(res.?);
}
pub fn tokenListSetValue(l: *TokenList, value: []const u8) !void {
const err = c.dom_tokenlist_set_value(l, try strFromData(value));
try DOMErr(err);
}
// ElementHTML
pub const ElementHTML = c.dom_html_element;

View File

@@ -690,6 +690,19 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return value.func.toValue();
}
if (s.is_tuple) {
// return the tuple struct as an array
var js_arr = v8.Array.init(isolate, @intCast(s.fields.len));
var js_obj = js_arr.castTo(v8.Object);
inline for (s.fields, 0..) |f, i| {
const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name));
if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
return error.FailedToCreateArray;
}
}
return js_obj.toValue();
}
// return the struct as a JS object
const js_obj = v8.Object.init(isolate);
inline for (s.fields) |f| {
@@ -888,6 +901,27 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
_ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 });
}
// Given an anytype, turns it into a v8.Object. The anytype could be:
// 1 - A V8.object already
// 2 - Our this JsObject wrapper around a V8.Object
// 3 - A zig instance that has previously been given to V8
// (i.e., the value has to be known to the executor)
fn valueToExistingObject(self: *const Executor, value: anytype) !v8.Object {
if (@TypeOf(value) == v8.Object) {
return value;
}
if (@TypeOf(value) == JsObject) {
return value.js_obj;
}
const persistent_object = self.scope.?.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
return persistent_object.castToObject();
}
// Wrap a v8.Value, largely so that we can provide a convenient
// toString function
fn createValue(self: *const Executor, value: v8.Value) Value {
@@ -989,7 +1023,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
gop.value_ptr.* = js_persistent;
if (@hasDecl(ptr.child, "postAttach")) {
const obj_wrap = JsObject{ .js_obj = js_obj, .executor = self };
const obj_wrap = JsThis{ .obj = .{ .js_obj = js_obj, .executor = self } };
switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) {
2 => try value.postAttach(obj_wrap),
3 => try value.postAttach(self.state, obj_wrap),
@@ -1080,7 +1114,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Callback = struct {
id: usize,
executor: *Executor,
this: ?v8.Object = null,
_this: ?v8.Object = null,
func: PersistentFunction,
// We use this when mapping a JS value to a Zig object. We can't
@@ -1096,22 +1130,23 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
};
pub fn setThis(self: *Callback, value: anytype) !void {
const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
self.this = persistent_object.castToObject();
self._this = try self.executor.valueToExistingObject(value);
}
pub fn call(self: *const Callback, args: anytype) !void {
return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args);
return self.callWithThis(self.getThis(), args);
}
pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void {
return self.tryCallWithThis(self.getThis(), args, result);
}
pub fn tryCallWithThis(self: *const Callback, this: anytype, args: anytype, result: *Result) !void {
var try_catch: TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
self.call(args) catch |err| {
self.callWithThis(this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.executor.scope.?.call_arena;
result.stack = try_catch.stack(allocator) catch null;
@@ -1124,9 +1159,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
};
}
fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void {
pub fn callWithThis(self: *const Callback, this: anytype, args: anytype) !void {
const executor = self.executor;
const js_this = try executor.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
var js_args: [fields.len]v8.Value = undefined;
@@ -1140,8 +1177,12 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
}
fn getThis(self: *const Callback) v8.Object {
return self._this orelse self.executor.context.getGlobal();
}
// debug/helper to print the source of the JS callback
fn printFunc(self: *const @This()) !void {
fn printFunc(self: Callback) !void {
const executor = self.executor;
const value = self.func.castToFunction().toValue();
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
@@ -1184,6 +1225,28 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
};
// This only exists so that we know whether a function wants the opaque
// JS argument (JsObject), or if it wants the receiver as an opaque
// value.
// JsObject is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// JsThis is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using JsThis.
pub const JsThis = struct {
obj: JsObject,
const _JSTHIS_ID_KLUDGE = true;
pub fn setIndex(self: JsThis, index: usize, value: anytype) !void {
return self.obj.setIndex(index, value);
}
pub fn set(self: JsThis, key: []const u8, value: anytype) !void {
return self.obj.set(key, value);
}
};
pub const TryCatch = struct {
inner: v8.TryCatch,
executor: *const Executor,
@@ -1747,14 +1810,14 @@ fn Caller(comptime E: type) type {
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a JsObject, set it, and exclude it
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isJsObject(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{
.handle = info.getThis(),
if (comptime isJsThis(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
.js_obj = info.getThis(),
.executor = self.executor,
};
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) {
@@ -1813,9 +1876,9 @@ fn Caller(comptime E: type) type {
}
if (comptime isState(param.type.?)) {
@compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ named_function.full_name);
} else if (comptime isJsObject(param.type.?)) {
@compileError("JsObject must be the last parameter: " ++ named_function.full_name);
@compileError("State must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
} else if (comptime isJsThis(param.type.?)) {
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
@@ -1915,9 +1978,20 @@ fn Caller(comptime E: type) type {
if (!js_value.isObject()) {
return error.InvalidArgument;
}
const js_obj = js_value.castTo(v8.Object);
if (comptime isJsObject(T)) {
// Caller wants an opaque JsObject. Probably a parameter
// that it needs to pass back into a callback
return E.JsObject{
.js_obj = js_obj,
.executor = self.executor,
};
}
const context = self.context;
const isolate = self.isolate;
const js_obj = js_value.castTo(v8.Object);
var value: T = undefined;
inline for (s.fields) |field| {
@@ -2003,6 +2077,10 @@ fn Caller(comptime E: type) type {
fn isJsObject(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE");
}
fn isJsThis(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSTHIS_ID_KLUDGE");
}
};
}