Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

document.getElementsByClassName #71

Merged
merged 10 commits into from
Nov 20, 2023
25 changes: 19 additions & 6 deletions src/dom/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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| {
Expand Down
102 changes: 78 additions & 24 deletions src/dom/html_collection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,91 @@ 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));
francisbouvier marked this conversation as resolved.
Show resolved Hide resolved
}
};

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 {
francisbouvier marked this conversation as resolved.
Show resolved Hide resolved
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.
// But we wanted a dynamically comparison here, according to the match tagname.
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,
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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
Expand All @@ -140,23 +199,18 @@ 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;
}

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");
Expand Down
23 changes: 23 additions & 0 deletions src/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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));
Expand Down
4 changes: 2 additions & 2 deletions test.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div id='content'>
<a id='link' href='foo'>OK</a>
<p id='para-empty'>
<a id='link' href='foo' class='ok'>OK</a>
<p id='para-empty' class='ok empty'>
<span id='para-empty-child'></span>
</p>
<p id='para'> And</p>
Expand Down
26 changes: 26 additions & 0 deletions tests/wpt/dom/Document-getElementsByClassName.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<title>Document.getElementsByClassName</title>
<link rel="author" title="Intel" href="http://www.intel.com">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
test(function() {
var a = document.createElement("a"),
b = document.createElement("b");
a.className = "foo";
this.add_cleanup(function() {document.body.removeChild(a);});
document.body.appendChild(a);

var l = document.getElementsByClassName("foo");
assert_true(l instanceof HTMLCollection);
assert_equals(l.length, 1);

b.className = "foo";
document.body.appendChild(b);
assert_equals(l.length, 2);

document.body.removeChild(b);
assert_equals(l.length, 1);
}, "getElementsByClassName() should be a live collection");
</script>
Loading