table deleteRow without full collection + add tests

This commit is contained in:
Karl Seguin
2026-07-15 20:56:28 +08:00
parent ecf7dd2137
commit 4365e01530
3 changed files with 158 additions and 40 deletions

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id="deleteRow">
{
// Built by hand so the tree order deliberately differs from the spec's
// row order: the tfoot comes first and bare tr children sit between
// the sections.
const table = document.createElement('table');
const rows = {};
const addRow = (parent, id) => {
const tr = document.createElement('tr');
tr.id = id;
parent.appendChild(tr);
rows[id] = tr;
};
const addSection = (tag, ...ids) => {
const section = document.createElement(tag);
for (const id of ids) addRow(section, id);
table.appendChild(section);
};
addSection('tfoot', 'foot1');
addRow(table, 'top1');
addSection('thead', 'head1', 'head2');
addSection('tbody', 'body1');
addRow(table, 'top2');
addSection('tbody', 'body2');
// Spec order: thead rows first, then table-level tr and tbody rows in
// tree order, then tfoot rows:
// head1, head2, top1, body1, top2, body2, foot1
// -1 removes the last row in spec order: the tfoot row, even though
// the tfoot is the table's first child.
table.deleteRow(-1);
testing.expectEqual(null, rows.foot1.parentNode);
// 6 rows left; out-of-range and < -1 both throw.
testing.expectError('IndexSizeError', () => table.deleteRow(6));
testing.expectError('IndexSizeError', () => table.deleteRow(-2));
// Index 3 of head1, head2, top1, body1, top2, body2 is the tbody row.
table.deleteRow(3);
testing.expectEqual(null, rows.body1.parentNode);
// deleteRow(0) drains the rest in spec order.
for (const id of ['head1', 'head2', 'top1', 'top2', 'body2']) {
testing.expectTrue(rows[id].parentNode !== null);
table.deleteRow(0);
testing.expectEqual(null, rows[id].parentNode);
}
// On a rowless table, -1 is a no-op but anything else throws.
table.deleteRow(-1);
testing.expectError('IndexSizeError', () => table.deleteRow(0));
}
</script>
<table id="tbodies">
<thead><tr></tr></thead>
<tbody id="b1"><tr></tr></tbody>
<tbody id="b2"><tr></tr></tbody>
<tfoot><tr></tr></tfoot>
</table>
<script id="tBodies">
{
const table = document.getElementById('tbodies');
testing.expectEqual(2, table.tBodies.length);
testing.expectEqual('b1', table.tBodies[0].id);
testing.expectEqual('b2', table.tBodies[1].id);
// The collection is live.
const bodies = table.tBodies;
table.removeChild(document.getElementById('b1'));
testing.expectEqual(1, bodies.length);
testing.expectEqual('b2', bodies[0].id);
}
</script>

View File

@@ -46,7 +46,9 @@ pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 {
var first = node;
while (first.previousSibling()) |prev| {
if (!isExclusiveTextNode(prev)) break;
if (!isExclusiveTextNode(prev)) {
break;
}
first = prev;
}
@@ -59,14 +61,17 @@ pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 {
var buf: std.ArrayList(u8) = .empty;
var current: ?*Node = first;
while (current) |cur| : (current = cur.nextSibling()) {
if (!isExclusiveTextNode(cur)) break;
try buf.appendSlice(frame.call_arena, cur._type.cdata._data.str());
if (!isExclusiveTextNode(cur)) {
break;
}
try buf.appendSlice(frame.local_arena, cur._type.cdata._data.str());
}
return buf.items;
}
fn isExclusiveTextNode(node: *const Node) bool {
return node._type == .cdata and node._type.cdata._type == .text;
fn isExclusiveTextNode(node: *Node) bool {
return node.is(Text) != null;
}
pub fn getAssignedSlot(self: *Text, frame: *Frame) ?*Slot {

View File

@@ -1,5 +1,3 @@
const std = @import("std");
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Frame = @import("../../../Frame.zig");
@@ -22,63 +20,92 @@ 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;
// Scratch only: deleteRow reads the list before the removal (the only
// point that can re-enter JS), so the local arena suffices.
const arena = frame.local_arena;
pub fn deleteRow(self: *Table, index: i32, frame: *Frame) !void {
if (index < -1) {
return error.IndexSizeError;
}
const row = self.findRow(index) orelse {
if (index == -1) {
// deleteRow(-1) on a rowless table is a no-op.
return;
}
return error.IndexSizeError;
};
_ = try row.parentNode().?.removeChild(row, frame);
}
try self.appendSectionRows(.thead, &rows, arena);
// Finds the index-th row (or the last row for -1) in spec order: thead, tr,
// tbody then tfoot
fn findRow(self: *Table, index: i32) ?*Node {
var scan: RowScan = .{ .index = index };
if (self.scanSectionRows(.thead, &scan)) |row| {
return row;
}
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),
.tr => if (scan.check(child)) |row| {
return row;
},
.tbody => if (scanChildRows(child, &scan)) |row| {
return row;
},
else => {},
}
}
try self.appendSectionRows(.tfoot, &rows, arena);
return rows;
if (self.scanSectionRows(.tfoot, &scan)) |row| {
return row;
}
if (index == -1) {
return scan.last;
}
return null;
}
fn appendSectionRows(self: *Table, tag: Element.Tag, rows: *std.ArrayList(*Node), arena: std.mem.Allocator) !void {
const RowScan = struct {
index: i32,
count: i32 = 0,
last: ?*Node = null,
fn check(self: *RowScan, row: *Node) ?*Node {
if (self.count == self.index) {
return row;
}
self.count += 1;
self.last = row;
return null;
}
};
fn scanSectionRows(self: *Table, tag: Element.Tag, scan: *RowScan) ?*Node {
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);
if (el.getTag() != tag) {
continue;
}
if (scanChildRows(child, scan)) |row| {
return row;
}
}
return null;
}
fn appendChildRows(section: *Node, rows: *std.ArrayList(*Node), arena: std.mem.Allocator) !void {
fn scanChildRows(section: *Node, scan: *RowScan) ?*Node {
var it = section.childrenIterator();
while (it.next()) |child| {
const el = child.is(Element) orelse continue;
if (el.getTag() == .tr) {
try rows.append(arena, child);
if (scan.check(child)) |row| {
return row;
}
}
}
}
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);
return null;
}
pub const JsApi = struct {
@@ -93,3 +120,8 @@ pub const JsApi = struct {
pub const tBodies = bridge.accessor(Table.getTBodies, null, .{});
pub const deleteRow = bridge.function(Table.deleteRow, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Table" {
try testing.htmlRunner("element/html/table.html", .{});
}