diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig
index 586bb9d5e..7706f944c 100644
--- a/src/browser/webapi/collections/HTMLCollection.zig
+++ b/src/browser/webapi/collections/HTMLCollection.zig
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const std = @import("std");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Element = @import("../Element.zig");
@@ -146,7 +147,7 @@ pub const JsApi = struct {
};
pub const length = bridge.accessor(HTMLCollection.length, null, .{});
- pub const @"[int]" = bridge.indexedReadWrite(HTMLCollection.getAtIndex, null, deleteAtIndex, null, null, .{ .null_as_undefined = true });
+ pub const @"[int]" = bridge.indexedReadWrite(HTMLCollection.getAtIndex, null, deleteAtIndex, null, getIndexes, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(struct {
pub fn wrap(self: *HTMLCollection, name: []const u8, frame: *Frame) !?*Element {
if (name.len == 0) {
@@ -155,7 +156,63 @@ pub const JsApi = struct {
return self.getByName(name, frame) orelse error.NotHandled;
}
- }.wrap, null, deleteByName, null, null, .{ .null_as_undefined = true });
+ }.wrap, null, deleteByName, getNames, null, .{ .null_as_undefined = true });
+
+ fn getIndexes(self: *HTMLCollection, exec: *const Execution) !js.Array {
+ const frame = switch (exec.js.global) {
+ .frame => |f| f,
+ .worker => unreachable,
+ };
+ const len = self.length(frame);
+ var arr = exec.js.local.?.newArray(len);
+ for (0..len) |i| {
+ _ = try arr.set(@intCast(i), i, .{});
+ }
+ return arr;
+ }
+
+ // The supported property names: for each element represented by the
+ // collection, in tree order, its id and (for HTML elements) its name
+ // attribute, skipping empty values and duplicates.
+ fn getNames(self: *HTMLCollection, exec: *const Execution) !js.Array {
+ const frame = switch (exec.js.global) {
+ .frame => |f| f,
+ .worker => unreachable,
+ };
+
+ var names: std.ArrayList([]const u8) = .{};
+ const arena = exec.call_arena;
+
+ const len = self.length(frame);
+ for (0..len) |i| {
+ const element = self.getAtIndex(i, frame) orelse break;
+ if (element.getAttributeSafe(comptime .wrap("id"))) |id| {
+ if (id.len > 0 and !contains(names.items, id)) {
+ try names.append(arena, id);
+ }
+ }
+ if (element._namespace == .html) {
+ if (element.getAttributeSafe(comptime .wrap("name"))) |name| {
+ if (name.len > 0 and !contains(names.items, name)) {
+ try names.append(arena, name);
+ }
+ }
+ }
+ }
+
+ var arr = exec.js.local.?.newArray(@intCast(names.items.len));
+ for (names.items, 0..) |name, i| {
+ _ = try arr.set(@intCast(i), name, .{});
+ }
+ return arr;
+ }
+
+ fn contains(names: []const []const u8, name: []const u8) bool {
+ for (names) |n| {
+ if (std.mem.eql(u8, n, name)) return true;
+ }
+ return false;
+ }
// Supported indexed and named properties can't be deleted (delete returns
// false, which throws a TypeError in strict mode); unsupported ones follow
diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig
index 8bc01915f..6383e7193 100644
--- a/src/browser/webapi/element/Attribute.zig
+++ b/src/browser/webapi/element/Attribute.zig
@@ -620,8 +620,26 @@ pub const NamedNodeMap = struct {
};
pub const length = bridge.accessor(NamedNodeMap.length, null, .{});
- pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, null, .{ .null_as_undefined = true });
- pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, null, null, .{ .null_as_undefined = true });
+ pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, getIndexes, .{ .null_as_undefined = true });
+ pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, getNames, null, .{ .null_as_undefined = true });
+
+ fn getIndexes(self: *const NamedNodeMap, exec: *const js.Execution) !js.Array {
+ const len = self.length();
+ var arr = exec.js.local.?.newArray(len);
+ for (0..len) |i| {
+ _ = try arr.set(@intCast(i), i, .{});
+ }
+ return arr;
+ }
+
+ fn getNames(self: *const NamedNodeMap, exec: *const js.Execution) !js.Array {
+ const names = try self.list().getNames(exec.call_arena);
+ var arr = exec.js.local.?.newArray(@intCast(names.len));
+ for (names, 0..) |name, i| {
+ _ = try arr.set(@intCast(i), name, .{});
+ }
+ return arr;
+ }
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{ .ce_reactions = true });
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{ .ce_reactions = true });
diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig
index e00355e9b..64d276425 100644
--- a/src/browser/webapi/element/DOMStringMap.zig
+++ b/src/browser/webapi/element/DOMStringMap.zig
@@ -98,27 +98,26 @@ fn camelToKebab(arena: Allocator, camel: String) !String {
return try String.init(arena, result.items, .{});
}
-// data-foo-bar -> fooBar
+// data-foo-bar -> fooBar. Returns null for non data-* attributes. Per spec,
+// only a '-' followed by an ASCII lowercase letter is folded into an
+// uppercase letter; any other '-' (e.g. trailing) is kept as-is, and the
+// bare "data-" attribute maps to the empty name.
fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 {
if (!std.mem.startsWith(u8, kebab, "data-")) {
return null;
}
const data_part = kebab[5..]; // Skip "data-"
- if (data_part.len == 0) {
- return null;
- }
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(arena, data_part.len);
- var capitalize_next = false;
- for (data_part) |c| {
- if (c == '-') {
- capitalize_next = true;
- } else if (capitalize_next) {
- result.appendAssumeCapacity(std.ascii.toUpper(c));
- capitalize_next = false;
+ var i: usize = 0;
+ while (i < data_part.len) : (i += 1) {
+ const c = data_part[i];
+ if (c == '-' and i + 1 < data_part.len and std.ascii.isLower(data_part[i + 1])) {
+ result.appendAssumeCapacity(std.ascii.toUpper(data_part[i + 1]));
+ i += 1;
} else {
result.appendAssumeCapacity(c);
}
@@ -136,5 +135,21 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
- pub const @"[]" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, null, null, .{ .null_as_undefined = true, .ce_reactions = true });
+ pub const @"[]" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, getNames, null, .{ .null_as_undefined = true, .ce_reactions = true });
+
+ // The supported property names are the camel-cased names of the
+ // element's data-* attributes, in attribute order.
+ fn getNames(self: *DOMStringMap, exec: *const js.Execution) !js.Array {
+ var names: std.ArrayList([]const u8) = .empty;
+ for (try self._element._attributes.getNames(exec.call_arena)) |attr_name| {
+ const camel = (try kebabToCamel(exec.call_arena, attr_name)) orelse continue;
+ try names.append(exec.call_arena, camel);
+ }
+
+ var arr = exec.js.local.?.newArray(@intCast(names.items.len));
+ for (names.items, 0..) |name, i| {
+ _ = try arr.set(@intCast(i), name, .{});
+ }
+ return arr;
+ }
};