From 4f5a9f683327ae0450e48a5f8c1fa00a3358cdd2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 8 Dec 2023 12:11:11 +0100 Subject: [PATCH 1/6] dom: implement some element getters --- src/dom/element.zig | 23 ++++++++++++++++++++++- src/netsurf.zig | 21 ++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/dom/element.zig b/src/dom/element.zig index e7380e3d..b6b4138a 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -25,8 +25,20 @@ pub const Element = struct { // JS funcs // -------- + pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 { + return try parser.nodeGetNamespace(parser.elementToNode(self)); + } + + pub fn get_prefix(self: *parser.Element) !?[]const u8 { + return try parser.nodeGetPrefix(parser.elementToNode(self)); + } + pub fn get_localName(self: *parser.Element) ![]const u8 { - return try parser.elementLocalName(self); + return try parser.nodeLocalName(parser.elementToNode(self)); + } + + pub fn get_tagName(self: *parser.Element) ![]const u8 { + return try parser.nodeName(parser.elementToNode(self)); } pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { @@ -92,6 +104,15 @@ pub fn testExecFn( js_env: *jsruntime.Env, comptime _: []jsruntime.API, ) !void { + var getters = [_]Case{ + .{ .src = "let g = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "g.namespaceURI", .ex = "http://www.w3.org/1999/xhtml" }, + .{ .src = "g.prefix", .ex = "null" }, + .{ .src = "g.localName", .ex = "div" }, + .{ .src = "g.tagName", .ex = "DIV" }, + }; + try checkCases(js_env, &getters); + var attribute = [_]Case{ .{ .src = "let a = document.getElementById('content')", .ex = "undefined" }, .{ .src = "a.hasAttributes()", .ex = "true" }, diff --git a/src/netsurf.zig b/src/netsurf.zig index d32b2cee..ec2c60f0 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -725,6 +725,22 @@ pub fn nodeGetAttributes(node: *Node) !*NamedNodeMap { return res.?; } +pub fn nodeGetNamespace(node: *Node) !?[]const u8 { + var s: ?*String = undefined; + const err = nodeVtable(node).dom_node_get_namespace.?(node, &s); + try DOMErr(err); + if (s == null) return null; + return strToData(s.?); +} + +pub fn nodeGetPrefix(node: *Node) !?[]const u8 { + var s: ?*String = undefined; + const err = nodeVtable(node).dom_node_get_prefix.?(node, &s); + try DOMErr(err); + if (s == null) return null; + return strToData(s.?); +} + // nodeToElement is an helper to convert a node to an element. pub inline fn nodeToElement(node: *Node) *Element { return @as(*Element, @ptrCast(node)); @@ -831,11 +847,6 @@ fn elementVtable(elem: *Element) c.dom_element_vtable { return getVtable(c.dom_element_vtable, Element, elem); } -pub fn elementLocalName(elem: *Element) ![]const u8 { - const node = @as(*Node, @ptrCast(elem)); - return try nodeLocalName(node); -} - pub fn elementGetAttribute(elem: *Element, name: []const u8) !?[]const u8 { var s: ?*String = undefined; const err = elementVtable(elem).dom_element_get_attribute.?(elem, try strFromData(name), &s); From 99caa1c4957fb1788c95d6db27587959db1eab92 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 8 Dec 2023 15:58:04 +0100 Subject: [PATCH 2/6] dom: implement element.id --- src/dom/element.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/dom/element.zig b/src/dom/element.zig index b6b4138a..517d56dd 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -41,6 +41,14 @@ pub const Element = struct { return try parser.nodeName(parser.elementToNode(self)); } + pub fn get_id(self: *parser.Element) ![]const u8 { + return try parser.elementGetAttribute(self, "id") orelse ""; + } + + pub fn set_id(self: *parser.Element, id: []const u8) !void { + return try parser.elementSetAttribute(self, "id", id); + } + pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { return try parser.nodeGetAttributes(parser.elementToNode(self)); } @@ -113,6 +121,15 @@ pub fn testExecFn( }; try checkCases(js_env, &getters); + var gettersetters = [_]Case{ + .{ .src = "let gs = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "gs.id", .ex = "content" }, + .{ .src = "gs.id = 'foo'", .ex = "foo" }, + .{ .src = "gs.id", .ex = "foo" }, + .{ .src = "gs.id = 'content'", .ex = "content" }, + }; + try checkCases(js_env, &gettersetters); + var attribute = [_]Case{ .{ .src = "let a = document.getElementById('content')", .ex = "undefined" }, .{ .src = "a.hasAttributes()", .ex = "true" }, From 3d5155bf2bad242f6e22670039e8f24d1d232213 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 8 Dec 2023 16:03:22 +0100 Subject: [PATCH 3/6] dom: implement element.className --- src/dom/element.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/dom/element.zig b/src/dom/element.zig index 517d56dd..2b0b1267 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -49,6 +49,14 @@ pub const Element = struct { return try parser.elementSetAttribute(self, "id", id); } + pub fn get_className(self: *parser.Element) ![]const u8 { + return try parser.elementGetAttribute(self, "class") orelse ""; + } + + pub fn set_className(self: *parser.Element, class: []const u8) !void { + return try parser.elementSetAttribute(self, "class", class); + } + pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { return try parser.nodeGetAttributes(parser.elementToNode(self)); } @@ -127,6 +135,12 @@ pub fn testExecFn( .{ .src = "gs.id = 'foo'", .ex = "foo" }, .{ .src = "gs.id", .ex = "foo" }, .{ .src = "gs.id = 'content'", .ex = "content" }, + .{ .src = "gs.className", .ex = "" }, + .{ .src = "let gs2 = document.getElementById('para-empty')", .ex = "undefined" }, + .{ .src = "gs2.className", .ex = "ok empty" }, + .{ .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" }, }; try checkCases(js_env, &gettersetters); From bd6f3faf9a34d5cd94b1cdf59bfa9877f6a360c7 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 8 Dec 2023 16:04:40 +0100 Subject: [PATCH 4/6] dom: implement element.slot --- src/dom/element.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/dom/element.zig b/src/dom/element.zig index 2b0b1267..0be21b99 100644 --- a/src/dom/element.zig +++ b/src/dom/element.zig @@ -57,6 +57,14 @@ pub const Element = struct { return try parser.elementSetAttribute(self, "class", class); } + pub fn get_slot(self: *parser.Element) ![]const u8 { + return try parser.elementGetAttribute(self, "slot") orelse ""; + } + + pub fn set_slot(self: *parser.Element, slot: []const u8) !void { + return try parser.elementSetAttribute(self, "slot", slot); + } + pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap { return try parser.nodeGetAttributes(parser.elementToNode(self)); } From 5c8d3eba3112b8626233c9513ac11c72b9c65f17 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 9 Dec 2023 10:47:40 +0100 Subject: [PATCH 5/6] 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 + + +
+ From dde13c42cf3ecf159affb940fb9545752731e582 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 12 Dec 2023 16:41:38 +0100 Subject: [PATCH 6/6] dom: use DOMError.InvalidCharacter --- src/dom/token_list.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dom/token_list.zig b/src/dom/token_list.zig index 42e1c73a..4dae7b67 100644 --- a/src/dom/token_list.zig +++ b/src/dom/token_list.zig @@ -49,7 +49,7 @@ pub const DOMTokenList = struct { return parser.DOMError.Syntax; } for (token) |c| { - if (std.ascii.isWhitespace(c)) return error.InvalidCharacter; + if (std.ascii.isWhitespace(c)) return parser.DOMError.InvalidCharacter; } }