diff --git a/src/browser/tests/element/html/table.html b/src/browser/tests/element/html/table.html new file mode 100644 index 000000000..6c37dcfcf --- /dev/null +++ b/src/browser/tests/element/html/table.html @@ -0,0 +1,81 @@ + + + + + + + + + + +
+ + diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig index 81cb7c6ad..063473ae5 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -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 { diff --git a/src/browser/webapi/element/html/Table.zig b/src/browser/webapi/element/html/Table.zig index 056e1015b..ccecf8d94 100644 --- a/src/browser/webapi/element/html/Table.zig +++ b/src/browser/webapi/element/html/Table.zig @@ -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", .{}); +}