From a2291b0713b1e99c36db3a09a026778c105d8f88 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 18 Apr 2025 19:10:37 +0800 Subject: [PATCH] Add missing TokenList APIs Add value setter, keys(), values(), entries() and forEach(). Like nodelist, forEach still doesn't support `this` arg (gotta think about how to do this). I think these iterable methods are missing in a few places, so I added a generic Entries iterator and a generic Iterable. jsruntime will now map a Zig tuple to a JS array. --- src/browser/dom/dom.zig | 4 +- src/browser/dom/token_list.zig | 92 +++++++++++++- src/browser/iterator/iterator.zig | 197 ++++++++++++++++++++++++++++-- src/browser/netsurf.zig | 5 + src/runtime/js.zig | 13 ++ 5 files changed, 298 insertions(+), 13 deletions(-) diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index 07e57c06..c0099a04 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -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, diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index 0472e81e..361ef047 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -19,9 +19,21 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); +const iterator = @import("../iterator/iterator.zig"); +const Callback = @import("../env.zig").Callback; +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 +110,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) !void { + var entries = _entries(self); + while (try entries._next()) |entry| { + var result: Callback.Result = undefined; + cbk.tryCall(.{ 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 +215,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" }, + }, .{}); } diff --git a/src/browser/iterator/iterator.zig b/src/browser/iterator/iterator.zig index aa248509..cb7a1c04 100644 --- a/src/browser/iterator/iterator.zig +++ b/src/browser/iterator/iterator.zig @@ -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, + }; + } +}; diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index eef41155..3a7decdd 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1674,6 +1674,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; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index e7233966..327cd579 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -691,6 +691,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| {