From 5c8d3eba3112b8626233c9513ac11c72b9c65f17 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 9 Dec 2023 10:47:40 +0100 Subject: [PATCH] dom: implement elemnt.classList and DOMTokenList --- src/dom/dom.zig | 2 + src/dom/element.zig | 6 + src/dom/token_list.zig | 147 +++++++ src/netsurf.zig | 50 +++ src/run_tests.zig | 2 + tests/wpt/dom/nodes/Element-classlist.html | 478 +++++++++++++++++++++ 6 files changed, 685 insertions(+) create mode 100644 src/dom/token_list.zig create mode 100644 tests/wpt/dom/nodes/Element-classlist.html diff --git a/src/dom/dom.zig b/src/dom/dom.zig index 564dc7ca..9c5c269f 100644 --- a/src/dom/dom.zig +++ b/src/dom/dom.zig @@ -4,6 +4,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 Nod = @import("node.zig"); pub const Interfaces = generate.Tuple(.{ @@ -11,6 +12,7 @@ pub const Interfaces = generate.Tuple(.{ EventTarget, DOMImplementation, NamedNodeMap, + DOMTokenList, Nod.Node, Nod.Interfaces, }); diff --git a/src/dom/element.zig b/src/dom/element.zig index 0be21b99..aa8bc2f5 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -65,6 +65,10 @@ pub const Element = struct { return try parser.elementSetAttribute(self, "slot", slot); } + pub fn get_classList(self: *parser.Element) !*parser.TokenList { + return try parser.tokenListCreate(self, "class"); + } + pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { return try parser.nodeGetAttributes(parser.elementToNode(self)); } @@ -149,6 +153,8 @@ pub fn testExecFn( .{ .src = "gs2.className = 'foo bar baz'", .ex = "foo bar baz" }, .{ .src = "gs2.className", .ex = "foo bar baz" }, .{ .src = "gs2.className = 'ok empty'", .ex = "ok empty" }, + .{ .src = "let cl = gs2.classList", .ex = "undefined" }, + .{ .src = "cl.length", .ex = "2" }, }; try checkCases(js_env, &gettersetters); diff --git a/src/dom/token_list.zig b/src/dom/token_list.zig new file mode 100644 index 00000000..42e1c73a --- /dev/null +++ b/src/dom/token_list.zig @@ -0,0 +1,147 @@ +const std = @import("std"); + +const parser = @import("../netsurf.zig"); + +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; +const Variadic = jsruntime.Variadic; + +const DOMException = @import("exceptions.zig").DOMException; + +// https://dom.spec.whatwg.org/#domtokenlist +pub const DOMTokenList = struct { + pub const Self = parser.TokenList; + pub const Exception = DOMException; + pub const mem_guarantied = true; + + pub fn get_length(self: *parser.TokenList) !u32 { + return parser.tokenListGetLength(self); + } + + pub fn _item(self: *parser.TokenList, index: u32) !?[]const u8 { + return parser.tokenListItem(self, index); + } + + pub fn _contains(self: *parser.TokenList, token: []const u8) !bool { + return parser.tokenListContains(self, token); + } + + pub fn _add(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { + if (tokens == null) return; + for (tokens.?.slice) |token| { + try parser.tokenListAdd(self, token); + } + } + + pub fn _remove(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { + if (tokens == null) return; + for (tokens.?.slice) |token| { + try parser.tokenListRemove(self, token); + } + } + + /// If token is the empty string, then throw a "SyntaxError" DOMException. + /// If token contains any ASCII whitespace, then throw an + /// "InvalidCharacterError" DOMException. + fn validateToken(token: []const u8) !void { + if (token.len == 0) { + return parser.DOMError.Syntax; + } + for (token) |c| { + if (std.ascii.isWhitespace(c)) return error.InvalidCharacter; + } + } + + pub fn _toggle(self: *parser.TokenList, token: []const u8, force: ?bool) !bool { + try validateToken(token); + const exists = try parser.tokenListContains(self, token); + if (exists) { + if (force == null or force.? == false) { + try parser.tokenListRemove(self, token); + return false; + } + return true; + } + + if (force == null or force.? == true) { + try parser.tokenListAdd(self, token); + return true; + } + return false; + } + + pub fn _replace(self: *parser.TokenList, token: []const u8, new: []const u8) !bool { + try validateToken(token); + try validateToken(new); + const exists = try parser.tokenListContains(self, token); + if (!exists) return false; + try parser.tokenListRemove(self, token); + try parser.tokenListAdd(self, new); + return true; + } + + // TODO to implement. + pub fn _supports(_: *parser.TokenList, token: []const u8) !bool { + try validateToken(token); + return error.TypeError; + } + + pub fn get_value(self: *parser.TokenList) !?[]const u8 { + return try parser.tokenListGetValue(self); + } +}; + +// Tests +// ----- + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, + comptime _: []jsruntime.API, +) !void { + var dynamiclist = [_]Case{ + .{ .src = "let gs = document.getElementById('para-empty')", .ex = "undefined" }, + .{ .src = "let cl = gs.classList", .ex = "undefined" }, + .{ .src = "gs.className", .ex = "ok empty" }, + .{ .src = "cl.value", .ex = "ok empty" }, + .{ .src = "cl.length", .ex = "2" }, + .{ .src = "gs.className = 'foo bar baz'", .ex = "foo bar baz" }, + .{ .src = "gs.className", .ex = "foo bar baz" }, + .{ .src = "cl.length", .ex = "3" }, + .{ .src = "gs.className = 'ok empty'", .ex = "ok empty" }, + .{ .src = "cl.length", .ex = "2" }, + }; + try checkCases(js_env, &dynamiclist); + + var testcases = [_]Case{ + .{ .src = "let cl2 = gs.classList", .ex = "undefined" }, + .{ .src = "cl2.length", .ex = "2" }, + .{ .src = "cl2.item(0)", .ex = "ok" }, + .{ .src = "cl2.item(1)", .ex = "empty" }, + .{ .src = "cl2.contains('ok')", .ex = "true" }, + .{ .src = "cl2.contains('nok')", .ex = "false" }, + .{ .src = "cl2.add('foo', 'bar', 'baz')", .ex = "undefined" }, + .{ .src = "cl2.length", .ex = "5" }, + .{ .src = "cl2.remove('foo', 'bar', 'baz')", .ex = "undefined" }, + .{ .src = "cl2.length", .ex = "2" }, + }; + try checkCases(js_env, &testcases); + + var toogle = [_]Case{ + .{ .src = "let cl3 = gs.classList", .ex = "undefined" }, + .{ .src = "cl3.toggle('ok')", .ex = "false" }, + .{ .src = "cl3.toggle('ok')", .ex = "true" }, + .{ .src = "cl3.length", .ex = "2" }, + }; + try checkCases(js_env, &toogle); + + var replace = [_]Case{ + .{ .src = "let cl4 = gs.classList", .ex = "undefined" }, + .{ .src = "cl4.replace('ok', 'nok')", .ex = "true" }, + .{ .src = "cl4.value", .ex = "empty nok" }, + .{ .src = "cl4.replace('nok', 'ok')", .ex = "true" }, + .{ .src = "cl4.value", .ex = "empty ok" }, + }; + try checkCases(js_env, &replace); +} diff --git a/src/netsurf.zig b/src/netsurf.zig index ec2c60f0..3f8336ff 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -893,6 +893,56 @@ pub inline fn elementToNode(e: *Element) *Node { return @as(*Node, @ptrCast(e)); } +// TokenList +pub const TokenList = c.dom_tokenlist; + +pub fn tokenListCreate(elt: *Element, attr: []const u8) !*TokenList { + var list: ?*TokenList = undefined; + const err = c.dom_tokenlist_create(elt, try strFromData(attr), &list); + try DOMErr(err); + return list.?; +} + +pub fn tokenListGetLength(l: *TokenList) !u32 { + var res: u32 = undefined; + const err = c.dom_tokenlist_get_length(l, &res); + try DOMErr(err); + return res; +} + +pub fn tokenListItem(l: *TokenList, index: u32) !?[]const u8 { + var res: ?*String = undefined; + const err = c._dom_tokenlist_item(l, index, &res); + try DOMErr(err); + if (res == null) return null; + return strToData(res.?); +} + +pub fn tokenListContains(l: *TokenList, token: []const u8) !bool { + var res: bool = undefined; + const err = c.dom_tokenlist_contains(l, try strFromData(token), &res); + try DOMErr(err); + return res; +} + +pub fn tokenListAdd(l: *TokenList, token: []const u8) !void { + const err = c.dom_tokenlist_add(l, try strFromData(token)); + try DOMErr(err); +} + +pub fn tokenListRemove(l: *TokenList, token: []const u8) !void { + const err = c.dom_tokenlist_remove(l, try strFromData(token)); + try DOMErr(err); +} + +pub fn tokenListGetValue(l: *TokenList) !?[]const u8 { + var res: ?*String = undefined; + const err = c.dom_tokenlist_get_value(l, &res); + try DOMErr(err); + if (res == null) return null; + return strToData(res.?); +} + // ElementHTML pub const ElementHTML = c.dom_html_element; diff --git a/src/run_tests.zig b/src/run_tests.zig index 58520055..681e96c0 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -16,6 +16,7 @@ const HTMLCollectionTestExecFn = @import("dom/html_collection.zig").testExecFn; const DOMExceptionTestExecFn = @import("dom/exceptions.zig").testExecFn; const DOMImplementationExecFn = @import("dom/implementation.zig").testExecFn; const NamedNodeMapExecFn = @import("dom/namednodemap.zig").testExecFn; +const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn; var doc: *parser.DocumentHTML = undefined; @@ -63,6 +64,7 @@ fn testsAllExecFn( DOMExceptionTestExecFn, DOMImplementationExecFn, NamedNodeMapExecFn, + DOMTokenListExecFn, }; inline for (testFns) |testFn| { diff --git a/tests/wpt/dom/nodes/Element-classlist.html b/tests/wpt/dom/nodes/Element-classlist.html new file mode 100644 index 00000000..2b5a271b --- /dev/null +++ b/tests/wpt/dom/nodes/Element-classlist.html @@ -0,0 +1,478 @@ + + +Test for the classList element attribute + + +
+