Skip to content

Commit a2291b0

Browse files
committed
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.
1 parent 581a79f commit a2291b0

File tree

5 files changed

+298
-13
lines changed

5 files changed

+298
-13
lines changed

src/browser/dom/dom.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const DOMException = @import("exceptions.zig").DOMException;
2020
const EventTarget = @import("event_target.zig").EventTarget;
2121
const DOMImplementation = @import("implementation.zig").DOMImplementation;
2222
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
23-
const DOMTokenList = @import("token_list.zig").DOMTokenList;
23+
const DOMTokenList = @import("token_list.zig");
2424
const NodeList = @import("nodelist.zig");
2525
const Node = @import("node.zig");
2626
const MutationObserver = @import("mutation_observer.zig");
@@ -30,7 +30,7 @@ pub const Interfaces = .{
3030
EventTarget,
3131
DOMImplementation,
3232
NamedNodeMap,
33-
DOMTokenList,
33+
DOMTokenList.Interfaces,
3434
NodeList.Interfaces,
3535
Node.Node,
3636
Node.Interfaces,

src/browser/dom/token_list.zig

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,21 @@
1919
const std = @import("std");
2020

2121
const parser = @import("../netsurf.zig");
22+
const iterator = @import("../iterator/iterator.zig");
2223

24+
const Callback = @import("../env.zig").Callback;
25+
const SessionState = @import("../env.zig").SessionState;
2326
const DOMException = @import("exceptions.zig").DOMException;
2427

28+
const log = std.log.scoped(.token_list);
29+
30+
pub const Interfaces = .{
31+
DOMTokenList,
32+
DOMTokenListIterable,
33+
TokenListEntriesIterator,
34+
TokenListEntriesIterator.Iterable,
35+
};
36+
2537
// https://dom.spec.whatwg.org/#domtokenlist
2638
pub const DOMTokenList = struct {
2739
pub const Self = parser.TokenList;
@@ -98,7 +110,60 @@ pub const DOMTokenList = struct {
98110
}
99111

100112
pub fn get_value(self: *parser.TokenList) !?[]const u8 {
101-
return try parser.tokenListGetValue(self);
113+
return (try parser.tokenListGetValue(self)) orelse "";
114+
}
115+
116+
pub fn set_value(self: *parser.TokenList, value: []const u8) !void {
117+
return parser.tokenListSetValue(self, value);
118+
}
119+
120+
pub fn _toString(self: *parser.TokenList) ![]const u8 {
121+
return (try get_value(self)) orelse "";
122+
}
123+
124+
pub fn _keys(self: *parser.TokenList) !iterator.U32Iterator {
125+
return .{ .length = try get_length(self) };
126+
}
127+
128+
pub fn _values(self: *parser.TokenList) DOMTokenListIterable {
129+
return DOMTokenListIterable.init(.{ .token_list = self });
130+
}
131+
132+
pub fn _entries(self: *parser.TokenList) TokenListEntriesIterator {
133+
return TokenListEntriesIterator.init(.{ .token_list = self });
134+
}
135+
136+
pub fn _symbol_iterator(self: *parser.TokenList) DOMTokenListIterable {
137+
return _values(self);
138+
}
139+
140+
// TODO handle thisArg
141+
pub fn _forEach(self: *parser.TokenList, cbk: Callback) !void {
142+
var entries = _entries(self);
143+
while (try entries._next()) |entry| {
144+
var result: Callback.Result = undefined;
145+
cbk.tryCall(.{ entry.@"1", entry.@"0", self }, &result) catch {
146+
log.err("callback error: {s}", .{result.exception});
147+
log.debug("stack:\n{s}", .{result.stack orelse "???"});
148+
};
149+
}
150+
}
151+
};
152+
153+
const DOMTokenListIterable = iterator.Iterable(Iterator, "DOMTokenListIterable");
154+
const TokenListEntriesIterator = iterator.NumericEntries(Iterator, "TokenListEntriesIterator");
155+
156+
pub const Iterator = struct {
157+
index: u32 = 0,
158+
token_list: *parser.TokenList,
159+
160+
// used when wrapped in an iterator.NumericEntries
161+
pub const Error = parser.DOMError;
162+
163+
pub fn _next(self: *Iterator) !?[]const u8 {
164+
const index = self.index;
165+
self.index = index + 1;
166+
return DOMTokenList._item(self.token_list, index);
102167
}
103168
};
104169

@@ -150,4 +215,29 @@ test "Browser.DOM.TokenList" {
150215
.{ "cl4.replace('nok', 'ok')", "true" },
151216
.{ "cl4.value", "empty ok" },
152217
}, .{});
218+
219+
try runner.testCases(&.{
220+
.{ "let cl5 = gs.classList", "undefined" },
221+
.{ "let keys = [...cl5.keys()]", "undefined" },
222+
.{ "keys.length", "2" },
223+
.{ "keys[0]", "0" },
224+
.{ "keys[1]", "1" },
225+
226+
.{ "let values = [...cl5.values()]", "undefined" },
227+
.{ "values.length", "2" },
228+
.{ "values[0]", "empty" },
229+
.{ "values[1]", "ok" },
230+
231+
.{ "let entries = [...cl5.entries()]", "undefined" },
232+
.{ "entries.length", "2" },
233+
.{ "entries[0]", "0,empty" },
234+
.{ "entries[1]", "1,ok" },
235+
}, .{});
236+
237+
try runner.testCases(&.{
238+
.{ "let cl6 = gs.classList", "undefined" },
239+
.{ "cl6.value = 'a b ccc'", "a b ccc" },
240+
.{ "cl6.value", "a b ccc" },
241+
.{ "cl6.toString()", "a b ccc" },
242+
}, .{});
153243
}

src/browser/iterator/iterator.zig

Lines changed: 187 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,201 @@ pub const U32Iterator = struct {
2828
.done = false,
2929
};
3030
}
31+
32+
// Iterators should be iterable. There's a [JS] example on MDN that
33+
// suggests this is the correct approach:
34+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
35+
pub fn _symbol_iterator(self: *U32Iterator) *U32Iterator {
36+
return self;
37+
}
3138
};
3239

33-
const testing = std.testing;
34-
test "U32Iterator" {
35-
const Return = U32Iterator.Return;
40+
// A wrapper around an iterator that emits an Iterable result
41+
// An iterable has a next() which emits a {done: bool, value: T} result
42+
pub fn Iterable(comptime T: type, comptime JsName: []const u8) type {
43+
// The inner iterator's return type.
44+
// Maybe an error union.
45+
// Definitely an optional
46+
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
47+
const CanError = @typeInfo(RawValue) == .error_union;
48+
49+
const Value = blk: {
50+
// Unwrap the RawValue
51+
var V = RawValue;
52+
if (CanError) {
53+
V = @typeInfo(V).error_union.payload;
54+
}
55+
break :blk @typeInfo(V).optional.child;
56+
};
57+
58+
const Result = struct {
59+
done: bool,
60+
// todo, technically, we should return undefined when done = true
61+
// or even omit the value;
62+
value: ?Value,
63+
};
64+
65+
const ReturnType = if (CanError) T.Error!Result else Result;
66+
67+
return struct {
68+
// the inner value iterator
69+
inner: T,
70+
71+
// Generics don't generate clean names. Can't just take the resulting
72+
// type name and use that as a the JS class name. So we always ask for
73+
// an explicit JS class name
74+
pub const js_name = JsName;
75+
76+
const Self = @This();
77+
78+
pub fn init(inner: T) Self {
79+
return .{ .inner = inner };
80+
}
81+
82+
pub fn _next(self: *Self) ReturnType {
83+
const value = if (comptime CanError) try self.inner._next() else self.inner._next();
84+
return .{ .done = value == null, .value = value };
85+
}
86+
87+
pub fn _symbol_iterator(self: *Self) *Self {
88+
return self;
89+
}
90+
};
91+
}
92+
93+
// A wrapper around an iterator that emits integer/index keyed entries.
94+
pub fn NumericEntries(comptime T: type, comptime JsName: []const u8) type {
95+
// The inner iterator's return type.
96+
// Maybe an error union.
97+
// Definitely an optional
98+
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
99+
const CanError = @typeInfo(RawValue) == .error_union;
100+
101+
const Value = blk: {
102+
// Unwrap the RawValue
103+
var V = RawValue;
104+
if (CanError) {
105+
V = @typeInfo(V).error_union.payload;
106+
}
107+
break :blk @typeInfo(V).optional.child;
108+
};
109+
110+
const ReturnType = if (CanError) T.Error!?struct { u32, Value } else ?struct { u32, Value };
111+
112+
// Avoid ambiguity. We want to expose a NumericEntries(T).Iterable, so we
113+
// need a declartion inside here for an "Iterable", but that will conflict
114+
// with the above Iterable generic function we have.
115+
const BaseIterable = Iterable;
116+
117+
return struct {
118+
// the inner value iterator
119+
inner: T,
120+
index: u32,
121+
122+
const Self = @This();
123+
124+
// Generics don't generate clean names. Can't just take the resulting
125+
// type name and use that as a the JS class name. So we always ask for
126+
// an explicit JS class name
127+
pub const js_name = JsName;
36128

129+
// re-exposed for when/if we compose this type into an Iterable
130+
pub const Error = T.Error;
131+
132+
// This iterator as an iterable
133+
pub const Iterable = BaseIterable(Self, JsName ++ "Iterable");
134+
135+
pub fn init(inner: T) Self {
136+
return .{ .inner = inner, .index = 0 };
137+
}
138+
139+
pub fn _next(self: *Self) ReturnType {
140+
const value_ = if (comptime CanError) try self.inner._next() else self.inner._next();
141+
const value = value_ orelse return null;
142+
143+
const index = self.index;
144+
self.index = index + 1;
145+
return .{ index, value };
146+
}
147+
148+
// make the iterator, iterable
149+
pub fn _symbol_iterator(self: *Self) Self.Iterable {
150+
return Self.Iterable.init(self.*);
151+
}
152+
};
153+
}
154+
155+
const testing = @import("../../testing.zig");
156+
test "U32Iterator" {
37157
{
38158
var it = U32Iterator{ .length = 0 };
39-
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
40-
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
159+
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
160+
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
41161
}
42162

43163
{
44164
var it = U32Iterator{ .length = 3 };
45-
try testing.expectEqual(Return{ .value = 0, .done = false }, it._next());
46-
try testing.expectEqual(Return{ .value = 1, .done = false }, it._next());
47-
try testing.expectEqual(Return{ .value = 2, .done = false }, it._next());
48-
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
49-
try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
165+
try testing.expectEqual(.{ .value = 0, .done = false }, it._next());
166+
try testing.expectEqual(.{ .value = 1, .done = false }, it._next());
167+
try testing.expectEqual(.{ .value = 2, .done = false }, it._next());
168+
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
169+
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
50170
}
51171
}
172+
173+
test "NumericEntries" {
174+
const it = DummyIterator{};
175+
var entries = NumericEntries(DummyIterator, "DummyIterator").init(it);
176+
177+
const v1 = entries._next().?;
178+
try testing.expectEqual(0, v1.@"0");
179+
try testing.expectEqual("it's", v1.@"1");
180+
181+
const v2 = entries._next().?;
182+
try testing.expectEqual(1, v2.@"0");
183+
try testing.expectEqual("over", v2.@"1");
184+
185+
const v3 = entries._next().?;
186+
try testing.expectEqual(2, v3.@"0");
187+
try testing.expectEqual("9000!!", v3.@"1");
188+
189+
try testing.expectEqual(null, entries._next());
190+
try testing.expectEqual(null, entries._next());
191+
try testing.expectEqual(null, entries._next());
192+
}
193+
194+
test "Iterable" {
195+
const it = DummyIterator{};
196+
var entries = Iterable(DummyIterator, "DummyIterator").init(it);
197+
198+
const v1 = entries._next();
199+
try testing.expectEqual(false, v1.done);
200+
try testing.expectEqual("it's", v1.value.?);
201+
202+
const v2 = entries._next();
203+
try testing.expectEqual(false, v2.done);
204+
try testing.expectEqual("over", v2.value.?);
205+
206+
const v3 = entries._next();
207+
try testing.expectEqual(false, v3.done);
208+
try testing.expectEqual("9000!!", v3.value.?);
209+
210+
try testing.expectEqual(true, entries._next().done);
211+
try testing.expectEqual(true, entries._next().done);
212+
try testing.expectEqual(true, entries._next().done);
213+
}
214+
215+
const DummyIterator = struct {
216+
index: u32 = 0,
217+
218+
pub fn _next(self: *DummyIterator) ?[]const u8 {
219+
const index = self.index;
220+
self.index = index + 1;
221+
return switch (index) {
222+
0 => "it's",
223+
1 => "over",
224+
2 => "9000!!",
225+
else => null,
226+
};
227+
}
228+
};

src/browser/netsurf.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,11 @@ pub fn tokenListGetValue(l: *TokenList) !?[]const u8 {
16741674
return strToData(res.?);
16751675
}
16761676

1677+
pub fn tokenListSetValue(l: *TokenList, value: []const u8) !void {
1678+
const err = c.dom_tokenlist_set_value(l, try strFromData(value));
1679+
try DOMErr(err);
1680+
}
1681+
16771682
// ElementHTML
16781683
pub const ElementHTML = c.dom_html_element;
16791684

src/runtime/js.zig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,19 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
691691
return value.func.toValue();
692692
}
693693

694+
if (s.is_tuple) {
695+
// return the tuple struct as an array
696+
var js_arr = v8.Array.init(isolate, @intCast(s.fields.len));
697+
var js_obj = js_arr.castTo(v8.Object);
698+
inline for (s.fields, 0..) |f, i| {
699+
const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name));
700+
if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
701+
return error.FailedToCreateArray;
702+
}
703+
}
704+
return js_obj.toValue();
705+
}
706+
694707
// return the struct as a JS object
695708
const js_obj = v8.Object.init(isolate);
696709
inline for (s.fields) |f| {

0 commit comments

Comments
 (0)