diff --git a/src/dom/document.zig b/src/dom/document.zig index b889169b..b012eb37 100644 --- a/src/dom/document.zig +++ b/src/dom/document.zig @@ -7,7 +7,8 @@ const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; const Node = @import("node.zig").Node; -const HTMLCollection = @import("html_collection.zig").HTMLCollection; + +const collection = @import("html_collection.zig"); const Element = @import("element.zig").Element; const ElementUnion = @import("element.zig").Union; @@ -49,12 +50,14 @@ pub const Document = struct { // the spec changed to return an HTMLCollection instead. // That's why we reimplemented getElementsByTagName by using an // HTMLCollection in zig here. - pub fn _getElementsByTagName(self: *parser.Document, tag_name: []const u8) HTMLCollection { + pub fn _getElementsByTagName(self: *parser.Document, tag_name: []const u8) collection.HTMLCollection { const root = parser.documentGetDocumentElement(self); - return HTMLCollection{ - .root = parser.elementToNode(root), - .match = tag_name, - }; + return collection.HTMLCollectionByTagName(parser.elementToNode(root), tag_name); + } + + pub fn _getElementsByClassName(self: *parser.Document, classNames: []const u8) collection.HTMLCollection { + const root = parser.documentGetDocumentElement(self); + return collection.HTMLCollectionByClassName(parser.elementToNode(root), classNames); } }; @@ -93,6 +96,16 @@ pub fn testExecFn( }; try checkCases(js_env, &getElementsByTagName); + var getElementsByClassName = [_]Case{ + .{ .src = "let ok = document.getElementsByClassName('ok')", .ex = "undefined" }, + .{ .src = "ok.length", .ex = "2" }, + .{ .src = "let empty = document.getElementsByClassName('empty')", .ex = "undefined" }, + .{ .src = "empty.length", .ex = "1" }, + .{ .src = "let emptyok = document.getElementsByClassName('empty ok')", .ex = "undefined" }, + .{ .src = "emptyok.length", .ex = "1" }, + }; + try checkCases(js_env, &getElementsByClassName); + const tags = comptime parser.Tag.all(); comptime var createElements: [(tags.len) * 2]Case = undefined; inline for (tags, 0..) |tag, i| { diff --git a/src/dom/html_collection.zig b/src/dom/html_collection.zig index 10cfb78f..de04cad4 100644 --- a/src/dom/html_collection.zig +++ b/src/dom/html_collection.zig @@ -5,11 +5,81 @@ const parser = @import("../netsurf.zig"); const jsruntime = @import("jsruntime"); const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; +const generate = @import("../generate.zig"); const utils = @import("utils.z"); const Element = @import("element.zig").Element; const Union = @import("element.zig").Union; +const Matcher = union(enum) { + matchByTagName: MatchByTagName, + matchByClassName: MatchByClassName, + + pub fn match(self: Matcher, node: *parser.Node) bool { + switch (self) { + inline else => |case| return case.match(node), + } + } +}; + +pub const MatchByTagName = struct { + // tag is used to select node against their name. + // tag comparison is case insensitive. + tag: []const u8, + is_wildcard: bool, + + fn init(tag_name: []const u8) MatchByTagName { + return MatchByTagName{ + .tag = tag_name, + .is_wildcard = std.mem.eql(u8, tag_name, "*"), + }; + } + + pub fn match(self: MatchByTagName, node: *parser.Node) bool { + return self.is_wildcard or std.ascii.eqlIgnoreCase(self.tag, parser.nodeName(node)); + } +}; + +pub fn HTMLCollectionByTagName(root: *parser.Node, tag_name: []const u8) HTMLCollection { + return HTMLCollection{ + .root = root, + .matcher = Matcher{ + .matchByTagName = MatchByTagName.init(tag_name), + }, + }; +} + +pub const MatchByClassName = struct { + classNames: []const u8, + + fn init(classNames: []const u8) MatchByClassName { + return MatchByClassName{ + .classNames = classNames, + }; + } + + pub fn match(self: MatchByClassName, node: *parser.Node) bool { + var it = std.mem.splitAny(u8, self.classNames, " "); + const e = parser.nodeToElement(node); + while (it.next()) |c| { + if (!parser.elementHasClass(e, c)) { + return false; + } + } + + return true; + } +}; + +pub fn HTMLCollectionByClassName(root: *parser.Node, classNames: []const u8) HTMLCollection { + return HTMLCollection{ + .root = root, + .matcher = Matcher{ + .matchByClassName = MatchByClassName.init(classNames), + }, + }; +} + // WEB IDL https://dom.spec.whatwg.org/#htmlcollection // HTMLCollection is re implemented in zig here because libdom // dom_html_collection expects a comparison function callback as arguement. @@ -17,10 +87,9 @@ const Union = @import("element.zig").Union; pub const HTMLCollection = struct { pub const mem_guarantied = true; + matcher: Matcher, + root: *parser.Node, - // match is used to select node against their name. - // match comparison is case insensitive. - match: []const u8, // save a state for the collection to improve the _item speed. cur_idx: ?u32 = undefined, @@ -76,20 +145,15 @@ pub const HTMLCollection = struct { /// get_length computes the collection's length dynamically according to /// the current root structure. // TODO: nodes retrieved must be de-referenced. - pub fn get_length(self: *HTMLCollection, allocator: std.mem.Allocator) !u32 { + pub fn get_length(self: *HTMLCollection) u32 { var len: u32 = 0; var node: *parser.Node = self.root; var ntype: parser.NodeType = undefined; - const imatch = try std.ascii.allocUpperString(allocator, self.match); - defer allocator.free(imatch); - - const is_wildcard = std.mem.eql(u8, self.match, "*"); - while (true) { ntype = parser.nodeType(node); if (ntype == .element) { - if (is_wildcard or std.mem.eql(u8, imatch, parser.nodeName(node))) { + if (self.matcher.match(node)) { len += 1; } } @@ -100,26 +164,21 @@ pub const HTMLCollection = struct { return len; } - pub fn _item(self: *HTMLCollection, allocator: std.mem.Allocator, index: u32) !?Union { + pub fn _item(self: *HTMLCollection, index: u32) ?Union { var i: u32 = 0; var node: *parser.Node = self.root; var ntype: parser.NodeType = undefined; - const is_wildcard = std.mem.eql(u8, self.match, "*"); - // Use the current state to improve speed if possible. if (self.cur_idx != null and index >= self.cur_idx.?) { i = self.cur_idx.?; node = self.cur_node.?; } - const imatch = try std.ascii.allocUpperString(allocator, self.match); - defer allocator.free(imatch); - while (true) { ntype = parser.nodeType(node); if (ntype == .element) { - if (is_wildcard or std.mem.eql(u8, imatch, parser.nodeName(node))) { + if (self.matcher.match(node)) { // check if we found the searched element. if (i == index) { // save the current state @@ -140,7 +199,7 @@ pub const HTMLCollection = struct { return null; } - pub fn _namedItem(self: *HTMLCollection, allocator: std.mem.Allocator, name: []const u8) !?Union { + pub fn _namedItem(self: *HTMLCollection, name: []const u8) ?Union { if (name.len == 0) { return null; } @@ -148,15 +207,10 @@ pub const HTMLCollection = struct { var node: *parser.Node = self.root; var ntype: parser.NodeType = undefined; - const is_wildcard = std.mem.eql(u8, self.match, "*"); - - const imatch = try std.ascii.allocUpperString(allocator, self.match); - defer allocator.free(imatch); - while (true) { ntype = parser.nodeType(node); if (ntype == .element) { - if (is_wildcard or std.mem.eql(u8, imatch, parser.nodeName(node))) { + if (self.matcher.match(node)) { const elem = @as(*parser.Element, @ptrCast(node)); var attr = parser.elementGetAttribute(elem, "id"); diff --git a/src/netsurf.zig b/src/netsurf.zig index 7052a0ac..c9af70a2 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -65,6 +65,18 @@ inline fn stringFromData(data: []const u8) *String { return s.?; } +const LWCString = c.lwc_string; + +// TODO implement lwcStringToData +// inline fn lwcStringToData(s: *LWCString) []const u8 { +// } + +inline fn lwcStringFromData(data: []const u8) *LWCString { + var s: ?*LWCString = undefined; + _ = c.lwc_intern_string(data.ptr, data.len, &s); + return s.?; +} + // Tag pub const Tag = enum(u8) { @@ -522,6 +534,11 @@ pub fn nodeReplaceChild(node: *Node, new_child: *Node, old_child: *Node) *Node { return res.?; } +// nodeToElement is an helper to convert a node to an element. +pub inline fn nodeToElement(node: *Node) *Element { + return @as(*Element, @ptrCast(node)); +} + // CharacterData pub const CharacterData = c.dom_characterdata; @@ -621,6 +638,12 @@ pub fn elementGetAttribute(elem: *Element, name: []const u8) ?[]const u8 { return stringToData(s.?); } +pub fn elementHasClass(elem: *Element, class: []const u8) bool { + var res: bool = undefined; + _ = elementVtable(elem).dom_element_has_class.?(elem, lwcStringFromData(class), &res); + return res; +} + // elementToNode is an helper to convert an element to a node. pub inline fn elementToNode(e: *Element) *Node { return @as(*Node, @ptrCast(e)); diff --git a/test.html b/test.html index cb54107e..7a0d7b77 100644 --- a/test.html +++ b/test.html @@ -1,6 +1,6 @@
- OK -

+ OK +

And

diff --git a/tests/wpt/dom/Document-getElementsByClassName.html b/tests/wpt/dom/Document-getElementsByClassName.html new file mode 100644 index 00000000..db8fac21 --- /dev/null +++ b/tests/wpt/dom/Document-getElementsByClassName.html @@ -0,0 +1,26 @@ + +Document.getElementsByClassName + + + +
+