mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
table deleteRow without full collection + add tests
This commit is contained in:
81
src/browser/tests/element/html/table.html
Normal file
81
src/browser/tests/element/html/table.html
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user