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