mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 18:59:36 -04:00
webapi: class tokens split on all ASCII whitespace; table row collections
Fixes 7 failing WPT /dom/nodes/getElementsByClassName-*.htm files: - Class attribute matching (getElementsByClassName and .class selectors) treated only the space character as a token separator, so class="a\nb" or class="a\tb" never matched "a". Per the spec the separators are ASCII whitespace (tab, LF, FF, CR, space); classAttributeContains now accepts all of them at token boundaries (deliberately not std.ascii.isWhitespace, which would also accept vertical tab). Fixes getElementsByClassName-02/04/15/22/25. - getElementsByClassName-20 exercised table.tBodies[0].rows[0].cells[0] and -21 table.deleteRow(1); the table interfaces were bare stubs. Added HTMLTableElement.tBodies (child tbody collection), HTMLTableSectionElement.rows (child tr collection), HTMLTableRowElement.cells (child td/th collection, a new `cells` NodeLive mode since child_tag filters a single tag), and HTMLTableElement.deleteRow with the spec's row ordering (thead rows, then table/tbody rows in tree order, then tfoot rows), -1 meaning the last row, and IndexSizeError for out-of-range indices. Coverage: getElementsByClassName-02/04/15/20/21/22/25.htm each 0/1 -> 1/1 (fully green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ const Mode = enum {
|
||||
all_elements,
|
||||
child_elements,
|
||||
child_tag,
|
||||
cells,
|
||||
selected_options,
|
||||
links,
|
||||
anchors,
|
||||
@@ -49,6 +50,7 @@ _data: union(Mode) {
|
||||
all_elements: NodeLive(.all_elements),
|
||||
child_elements: NodeLive(.child_elements),
|
||||
child_tag: NodeLive(.child_tag),
|
||||
cells: NodeLive(.cells),
|
||||
selected_options: NodeLive(.selected_options),
|
||||
links: NodeLive(.links),
|
||||
anchors: NodeLive(.anchors),
|
||||
@@ -92,6 +94,7 @@ pub fn iterator(self: *HTMLCollection, exec: *const Execution) !*Iterator {
|
||||
.all_elements => |*impl| .{ .all_elements = impl._tw.clone() },
|
||||
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
|
||||
.child_tag => |*impl| .{ .child_tag = impl._tw.clone() },
|
||||
.cells => |*impl| .{ .cells = impl._tw.clone() },
|
||||
.selected_options => |*impl| .{ .selected_options = impl._tw.clone() },
|
||||
.links => |*impl| .{ .links = impl._tw.clone() },
|
||||
.anchors => |*impl| .{ .anchors = impl._tw.clone() },
|
||||
@@ -112,6 +115,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
all_elements: TreeWalker.FullExcludeSelf,
|
||||
child_elements: TreeWalker.Children,
|
||||
child_tag: TreeWalker.Children,
|
||||
cells: TreeWalker.Children,
|
||||
selected_options: TreeWalker.Children,
|
||||
links: TreeWalker.FullExcludeSelf,
|
||||
anchors: TreeWalker.FullExcludeSelf,
|
||||
@@ -128,6 +132,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
.all_elements => |*impl| impl.nextTw(&self.tw.all_elements),
|
||||
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
|
||||
.child_tag => |*impl| impl.nextTw(&self.tw.child_tag),
|
||||
.cells => |*impl| impl.nextTw(&self.tw.cells),
|
||||
.selected_options => |*impl| impl.nextTw(&self.tw.selected_options),
|
||||
.links => |*impl| impl.nextTw(&self.tw.links),
|
||||
.anchors => |*impl| impl.nextTw(&self.tw.anchors),
|
||||
|
||||
@@ -39,6 +39,7 @@ const Mode = enum {
|
||||
all_elements,
|
||||
child_elements,
|
||||
child_tag,
|
||||
cells,
|
||||
selected_options,
|
||||
links,
|
||||
anchors,
|
||||
@@ -59,6 +60,7 @@ const Filters = union(Mode) {
|
||||
all_elements,
|
||||
child_elements,
|
||||
child_tag: Element.Tag,
|
||||
cells,
|
||||
selected_options,
|
||||
links,
|
||||
anchors,
|
||||
@@ -91,7 +93,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
const Filter = Filters.TypeOf(mode);
|
||||
const TW = switch (mode) {
|
||||
.tag, .tag_name, .tag_name_ns, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
|
||||
.child_elements, .child_tag, .selected_options => TreeWalker.Children,
|
||||
.child_elements, .child_tag, .cells, .selected_options => TreeWalker.Children,
|
||||
};
|
||||
return struct {
|
||||
_tw: TW,
|
||||
@@ -283,6 +285,12 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
const el = node.is(Element) orelse return false;
|
||||
return el.getTag() == self._filter;
|
||||
},
|
||||
.cells => {
|
||||
// HTMLTableRowElement.cells: td and th children.
|
||||
const el = node.is(Element) orelse return false;
|
||||
const tag = el.getTag();
|
||||
return tag == .td or tag == .th;
|
||||
},
|
||||
.selected_options => {
|
||||
const el = node.is(Element) orelse return false;
|
||||
const Option = Element.Html.Option;
|
||||
@@ -373,6 +381,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
|
||||
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
|
||||
.child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },
|
||||
.cells => HTMLCollection{ ._data = .{ .cells = self } },
|
||||
.selected_options => HTMLCollection{ ._data = .{ .selected_options = self } },
|
||||
.links => HTMLCollection{ ._data = .{ .links = self } },
|
||||
.anchors => HTMLCollection{ ._data = .{ .anchors = self } },
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const collections = @import("../../collections.zig");
|
||||
|
||||
const Table = @This();
|
||||
|
||||
@@ -14,6 +18,67 @@ pub fn asNode(self: *Table) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getTBodies(self: *Table, frame: *Frame) collections.NodeLive(.child_tag) {
|
||||
return collections.NodeLive(.child_tag).init(self.asNode(), .tbody, frame);
|
||||
}
|
||||
|
||||
// The table's rows in spec order: rows of thead children first, then tr
|
||||
// children of the table and rows of tbody children in tree order, then rows
|
||||
// of tfoot children.
|
||||
fn collectRows(self: *Table, frame: *Frame) !std.ArrayList(*Node) {
|
||||
var rows: std.ArrayList(*Node) = .empty;
|
||||
const arena = frame.call_arena;
|
||||
|
||||
try self.appendSectionRows(.thead, &rows, arena);
|
||||
|
||||
var it = self.asNode().childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
const el = child.is(Element) orelse continue;
|
||||
switch (el.getTag()) {
|
||||
.tr => try rows.append(arena, child),
|
||||
.tbody => try appendChildRows(child, &rows, arena),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
try self.appendSectionRows(.tfoot, &rows, arena);
|
||||
return rows;
|
||||
}
|
||||
|
||||
fn appendSectionRows(self: *Table, tag: Element.Tag, rows: *std.ArrayList(*Node), arena: std.mem.Allocator) !void {
|
||||
var it = self.asNode().childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
const el = child.is(Element) orelse continue;
|
||||
if (el.getTag() != tag) continue;
|
||||
try appendChildRows(child, rows, arena);
|
||||
}
|
||||
}
|
||||
|
||||
fn appendChildRows(section: *Node, rows: *std.ArrayList(*Node), arena: std.mem.Allocator) !void {
|
||||
var it = section.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
const el = child.is(Element) orelse continue;
|
||||
if (el.getTag() == .tr) {
|
||||
try rows.append(arena, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deleteRow(self: *Table, index: i32, frame: *Frame) !void {
|
||||
const rows = try self.collectRows(frame);
|
||||
const len: i32 = @intCast(rows.items.len);
|
||||
const idx: i32 = if (index == -1) len - 1 else index;
|
||||
if (idx == -1 and index == -1) {
|
||||
// deleteRow(-1) on a rowless table is a no-op.
|
||||
return;
|
||||
}
|
||||
if (idx < 0 or idx >= len) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
const row = rows.items[@intCast(idx)];
|
||||
_ = try row.parentNode().?.removeChild(row, frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Table);
|
||||
|
||||
@@ -22,4 +87,7 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const tBodies = bridge.accessor(Table.getTBodies, null, .{});
|
||||
pub const deleteRow = bridge.function(Table.deleteRow, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const collections = @import("../../collections.zig");
|
||||
|
||||
const TableRow = @This();
|
||||
|
||||
@@ -14,6 +16,10 @@ pub fn asNode(self: *TableRow) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getCells(self: *TableRow, frame: *Frame) collections.NodeLive(.cells) {
|
||||
return collections.NodeLive(.cells).init(self.asNode(), {}, frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TableRow);
|
||||
|
||||
@@ -22,4 +28,6 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const cells = bridge.accessor(TableRow.getCells, null, .{});
|
||||
};
|
||||
|
||||
@@ -2,8 +2,10 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const collections = @import("../../collections.zig");
|
||||
|
||||
const String = lp.String;
|
||||
|
||||
@@ -20,6 +22,10 @@ pub fn asNode(self: *TableSection) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getRows(self: *TableSection, frame: *Frame) collections.NodeLive(.child_tag) {
|
||||
return collections.NodeLive(.child_tag).init(self.asNode(), .tr, frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TableSection);
|
||||
|
||||
@@ -28,4 +34,6 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const rows = bridge.accessor(TableSection.getRows, null, .{});
|
||||
};
|
||||
|
||||
@@ -187,9 +187,9 @@ pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bo
|
||||
|
||||
var search = class_attr;
|
||||
while (std.mem.indexOf(u8, search, class_name)) |pos| {
|
||||
const is_start = pos == 0 or search[pos - 1] == ' ';
|
||||
const is_start = pos == 0 or isClassWhitespace(search[pos - 1]);
|
||||
const end = pos + class_name.len;
|
||||
const is_end = end == search.len or search[end] == ' ';
|
||||
const is_end = end == search.len or isClassWhitespace(search[end]);
|
||||
|
||||
if (is_start and is_end) return true;
|
||||
|
||||
@@ -198,6 +198,15 @@ pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bo
|
||||
return false;
|
||||
}
|
||||
|
||||
// The class attribute tokens are separated by ASCII whitespace (which,
|
||||
// unlike std.ascii.isWhitespace, does not include vertical tab).
|
||||
fn isClassWhitespace(c: u8) bool {
|
||||
return switch (c) {
|
||||
'\t', '\n', 0x0C, '\r', ' ' => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Part = union(enum) {
|
||||
id: []const u8,
|
||||
class: []const u8,
|
||||
|
||||
Reference in New Issue
Block a user