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
+
+
+
+
+