mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
webapi: enumerate collection supported properties for ownPropertyNames
Fixes the Object.getOwnPropertyNames related failures in WPT /dom/collections: - HTMLCollection-live-mutations.window.html: "ownPropertyNames" case - HTMLCollection-supported-property-names.html: 3 getOwnPropertyNames cases - namednodemap-supported-property-names.html: all 3 cases - domstringmap-supported-property-names.html: all 5 cases Object.getOwnPropertyNames returned no interceptor-backed properties because these collections registered no enumerator callbacks. Using the bridge's existing indexed and named enumerator support: - HTMLCollection registers an indexed enumerator (the supported indices) and a named enumerator implementing the spec's supported property names: for each element in tree order, its non-empty id and, for HTML elements, its non-empty name attribute, without duplicates. - NamedNodeMap registers indexed and named enumerators (attribute qualified names in order, via List.getNames). - DOMStringMap registers a named enumerator (camel-cased data-* attribute names). The previously unused kebabToCamel helper is fixed to follow the spec conversion: only a '-' followed by an ASCII lowercase letter is folded to an uppercase letter, a bare "data-" attribute maps to the empty name, and a trailing '-' is preserved. (As originally written, this commit also added named enumerator interceptor support to the js bridge; main gained equivalent support independently, so the rebase keeps main's bridge API.) Coverage: - /dom/collections/HTMLCollection-live-mutations.window.html 4/5 -> 5/5 - /dom/collections/HTMLCollection-supported-property-names.html 2/6 -> 5/6 - /dom/collections/namednodemap-supported-property-names.html 0/3 -> 3/3 - /dom/collections/domstringmap-supported-property-names.html 0/5 -> 5/5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user