diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig
index 11683dac5..3765c2533 100644
--- a/src/browser/webapi/collections/HTMLCollection.zig
+++ b/src/browser/webapi/collections/HTMLCollection.zig
@@ -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),
diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig
index 82769cd07..69e30d178 100644
--- a/src/browser/webapi/collections/node_live.zig
+++ b/src/browser/webapi/collections/node_live.zig
@@ -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 } },
diff --git a/src/browser/webapi/element/html/Table.zig b/src/browser/webapi/element/html/Table.zig
index 27664fe30..43ea812b0 100644
--- a/src/browser/webapi/element/html/Table.zig
+++ b/src/browser/webapi/element/html/Table.zig
@@ -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 });
};
diff --git a/src/browser/webapi/element/html/TableRow.zig b/src/browser/webapi/element/html/TableRow.zig
index 33ede0d5d..9b63fd806 100644
--- a/src/browser/webapi/element/html/TableRow.zig
+++ b/src/browser/webapi/element/html/TableRow.zig
@@ -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, .{});
};
diff --git a/src/browser/webapi/element/html/TableSection.zig b/src/browser/webapi/element/html/TableSection.zig
index b4ed51180..05024b490 100644
--- a/src/browser/webapi/element/html/TableSection.zig
+++ b/src/browser/webapi/element/html/TableSection.zig
@@ -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, .{});
};
diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig
index 9bb3a9c4c..ec5c09532 100644
--- a/src/browser/webapi/selector/Selector.zig
+++ b/src/browser/webapi/selector/Selector.zig
@@ -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,