From 2e64c461c381f44027a17545afcb6459c64039ea Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 11:36:58 +0100 Subject: [PATCH 1/2] add attribute reflections for 8 HTML element types Wire up missing IDL properties for HTMLTimeElement (dateTime), HTMLLIElement (value), HTMLOListElement (start, reversed, type), HTMLOptGroupElement (disabled, label), HTMLQuoteElement (cite), HTMLTableCellElement (colSpan, rowSpan), HTMLLabelElement (htmlFor), and HTMLFieldSetElement (disabled, name). --- src/browser/tests/element/html/fieldset.html | 35 +++++++++++++ src/browser/tests/element/html/label.html | 18 +++++++ src/browser/tests/element/html/li.html | 23 +++++++++ src/browser/tests/element/html/ol.html | 51 +++++++++++++++++++ src/browser/tests/element/html/optgroup.html | 40 +++++++++++++++ src/browser/tests/element/html/quote.html | 17 +++++++ src/browser/tests/element/html/tablecell.html | 35 +++++++++++++ src/browser/tests/element/html/time.html | 17 +++++++ src/browser/webapi/element/html/FieldSet.zig | 29 +++++++++++ src/browser/webapi/element/html/LI.zig | 19 +++++++ src/browser/webapi/element/html/Label.zig | 16 ++++++ src/browser/webapi/element/html/OL.zig | 41 +++++++++++++++ src/browser/webapi/element/html/OptGroup.zig | 29 +++++++++++ src/browser/webapi/element/html/Quote.zig | 19 +++++++ src/browser/webapi/element/html/TableCell.zig | 30 +++++++++++ src/browser/webapi/element/html/Time.zig | 16 ++++++ 16 files changed, 435 insertions(+) create mode 100644 src/browser/tests/element/html/fieldset.html create mode 100644 src/browser/tests/element/html/label.html create mode 100644 src/browser/tests/element/html/li.html create mode 100644 src/browser/tests/element/html/ol.html create mode 100644 src/browser/tests/element/html/optgroup.html create mode 100644 src/browser/tests/element/html/quote.html create mode 100644 src/browser/tests/element/html/tablecell.html create mode 100644 src/browser/tests/element/html/time.html diff --git a/src/browser/tests/element/html/fieldset.html b/src/browser/tests/element/html/fieldset.html new file mode 100644 index 00000000..76c281d5 --- /dev/null +++ b/src/browser/tests/element/html/fieldset.html @@ -0,0 +1,35 @@ + + + +
+ +
+
+ +
+ + + + diff --git a/src/browser/tests/element/html/label.html b/src/browser/tests/element/html/label.html new file mode 100644 index 00000000..cdc3f637 --- /dev/null +++ b/src/browser/tests/element/html/label.html @@ -0,0 +1,18 @@ + + + + + + + diff --git a/src/browser/tests/element/html/li.html b/src/browser/tests/element/html/li.html new file mode 100644 index 00000000..62eea5dd --- /dev/null +++ b/src/browser/tests/element/html/li.html @@ -0,0 +1,23 @@ + + + +
    +
  1. Item
  2. +
  3. Item
  4. +
+ + diff --git a/src/browser/tests/element/html/ol.html b/src/browser/tests/element/html/ol.html new file mode 100644 index 00000000..8036e470 --- /dev/null +++ b/src/browser/tests/element/html/ol.html @@ -0,0 +1,51 @@ + + + +
    +
  1. Item
  2. +
+
    +
  1. Item
  2. +
+ + + + + + diff --git a/src/browser/tests/element/html/optgroup.html b/src/browser/tests/element/html/optgroup.html new file mode 100644 index 00000000..456f3de6 --- /dev/null +++ b/src/browser/tests/element/html/optgroup.html @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/src/browser/tests/element/html/quote.html b/src/browser/tests/element/html/quote.html new file mode 100644 index 00000000..72a4c3ee --- /dev/null +++ b/src/browser/tests/element/html/quote.html @@ -0,0 +1,17 @@ + + + +
Quote
+ + diff --git a/src/browser/tests/element/html/tablecell.html b/src/browser/tests/element/html/tablecell.html new file mode 100644 index 00000000..d1667288 --- /dev/null +++ b/src/browser/tests/element/html/tablecell.html @@ -0,0 +1,35 @@ + + + + + + + + +
CellCell
+ + + + diff --git a/src/browser/tests/element/html/time.html b/src/browser/tests/element/html/time.html new file mode 100644 index 00000000..b1642642 --- /dev/null +++ b/src/browser/tests/element/html/time.html @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/browser/webapi/element/html/FieldSet.zig b/src/browser/webapi/element/html/FieldSet.zig index 2b966e2f..1a71c92d 100644 --- a/src/browser/webapi/element/html/FieldSet.zig +++ b/src/browser/webapi/element/html/FieldSet.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,26 @@ pub fn asNode(self: *FieldSet) *Node { return self.asElement().asNode(); } +pub fn getDisabled(self: *FieldSet) bool { + return self.asElement().getAttributeSafe(comptime .wrap("disabled")) != null; +} + +pub fn setDisabled(self: *FieldSet, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page); + } else { + try self.asElement().removeAttribute(comptime .wrap("disabled"), page); + } +} + +pub fn getName(self: *FieldSet) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("name")) orelse ""; +} + +pub fn setName(self: *FieldSet, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(FieldSet); @@ -22,4 +43,12 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const disabled = bridge.accessor(FieldSet.getDisabled, FieldSet.setDisabled, .{}); + pub const name = bridge.accessor(FieldSet.getName, FieldSet.setName, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.FieldSet" { + try testing.htmlRunner("element/html/fieldset.html", .{}); +} diff --git a/src/browser/webapi/element/html/LI.zig b/src/browser/webapi/element/html/LI.zig index e0213020..9c7defbe 100644 --- a/src/browser/webapi/element/html/LI.zig +++ b/src/browser/webapi/element/html/LI.zig @@ -16,7 +16,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -31,6 +33,16 @@ pub fn asNode(self: *LI) *Node { return self.asElement().asNode(); } +pub fn getValue(self: *LI) i32 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("value")) orelse return 0; + return std.fmt.parseInt(i32, attr, 10) catch 0; +} + +pub fn setValue(self: *LI, value: i32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(str), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(LI); @@ -39,4 +51,11 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const value = bridge.accessor(LI.getValue, LI.setValue, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.LI" { + try testing.htmlRunner("element/html/li.html", .{}); +} diff --git a/src/browser/webapi/element/html/Label.zig b/src/browser/webapi/element/html/Label.zig index a9c0243f..c08bff06 100644 --- a/src/browser/webapi/element/html/Label.zig +++ b/src/browser/webapi/element/html/Label.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,14 @@ pub fn asNode(self: *Label) *Node { return self.asElement().asNode(); } +pub fn getHtmlFor(self: *Label) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("for")) orelse ""; +} + +pub fn setHtmlFor(self: *Label, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("for"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Label); @@ -22,4 +31,11 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const htmlFor = bridge.accessor(Label.getHtmlFor, Label.setHtmlFor, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Label" { + try testing.htmlRunner("element/html/label.html", .{}); +} diff --git a/src/browser/webapi/element/html/OL.zig b/src/browser/webapi/element/html/OL.zig index 09f5eeaa..7001ae24 100644 --- a/src/browser/webapi/element/html/OL.zig +++ b/src/browser/webapi/element/html/OL.zig @@ -16,7 +16,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -31,6 +33,36 @@ pub fn asNode(self: *OL) *Node { return self.asElement().asNode(); } +pub fn getStart(self: *OL) i32 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("start")) orelse return 1; + return std.fmt.parseInt(i32, attr, 10) catch 1; +} + +pub fn setStart(self: *OL, value: i32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe(comptime .wrap("start"), .wrap(str), page); +} + +pub fn getReversed(self: *OL) bool { + return self.asElement().getAttributeSafe(comptime .wrap("reversed")) != null; +} + +pub fn setReversed(self: *OL, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe(comptime .wrap("reversed"), .wrap(""), page); + } else { + try self.asElement().removeAttribute(comptime .wrap("reversed"), page); + } +} + +pub fn getType(self: *OL) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("type")) orelse ""; +} + +pub fn setType(self: *OL, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(OL); @@ -39,4 +71,13 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const start = bridge.accessor(OL.getStart, OL.setStart, .{}); + pub const reversed = bridge.accessor(OL.getReversed, OL.setReversed, .{}); + pub const @"type" = bridge.accessor(OL.getType, OL.setType, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.OL" { + try testing.htmlRunner("element/html/ol.html", .{}); +} diff --git a/src/browser/webapi/element/html/OptGroup.zig b/src/browser/webapi/element/html/OptGroup.zig index 2ccab870..07d99e0b 100644 --- a/src/browser/webapi/element/html/OptGroup.zig +++ b/src/browser/webapi/element/html/OptGroup.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,26 @@ pub fn asNode(self: *OptGroup) *Node { return self.asElement().asNode(); } +pub fn getDisabled(self: *OptGroup) bool { + return self.asElement().getAttributeSafe(comptime .wrap("disabled")) != null; +} + +pub fn setDisabled(self: *OptGroup, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page); + } else { + try self.asElement().removeAttribute(comptime .wrap("disabled"), page); + } +} + +pub fn getLabel(self: *OptGroup) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("label")) orelse ""; +} + +pub fn setLabel(self: *OptGroup, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("label"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(OptGroup); @@ -22,4 +43,12 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const disabled = bridge.accessor(OptGroup.getDisabled, OptGroup.setDisabled, .{}); + pub const label = bridge.accessor(OptGroup.getLabel, OptGroup.setLabel, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.OptGroup" { + try testing.htmlRunner("element/html/optgroup.html", .{}); +} diff --git a/src/browser/webapi/element/html/Quote.zig b/src/browser/webapi/element/html/Quote.zig index 9d1ef3af..2cd02e74 100644 --- a/src/browser/webapi/element/html/Quote.zig +++ b/src/browser/webapi/element/html/Quote.zig @@ -1,5 +1,6 @@ const String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -17,6 +18,17 @@ pub fn asNode(self: *Quote) *Node { return self.asElement().asNode(); } +pub fn getCite(self: *Quote, page: *Page) ![]const u8 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("cite")) orelse return ""; + if (attr.len == 0) return ""; + const URL = @import("../../URL.zig"); + return URL.resolve(page.call_arena, page.base(), attr, .{}); +} + +pub fn setCite(self: *Quote, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("cite"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Quote); @@ -25,4 +37,11 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const cite = bridge.accessor(Quote.getCite, Quote.setCite, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Quote" { + try testing.htmlRunner("element/html/quote.html", .{}); +} diff --git a/src/browser/webapi/element/html/TableCell.zig b/src/browser/webapi/element/html/TableCell.zig index 95ebedb0..b8ddccca 100644 --- a/src/browser/webapi/element/html/TableCell.zig +++ b/src/browser/webapi/element/html/TableCell.zig @@ -1,5 +1,7 @@ +const std = @import("std"); const String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -17,6 +19,26 @@ pub fn asNode(self: *TableCell) *Node { return self.asElement().asNode(); } +pub fn getColSpan(self: *TableCell) u32 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("colspan")) orelse return 1; + return std.fmt.parseUnsigned(u32, attr, 10) catch 1; +} + +pub fn setColSpan(self: *TableCell, value: u32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe(comptime .wrap("colspan"), .wrap(str), page); +} + +pub fn getRowSpan(self: *TableCell) u32 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("rowspan")) orelse return 1; + return std.fmt.parseUnsigned(u32, attr, 10) catch 1; +} + +pub fn setRowSpan(self: *TableCell, value: u32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe(comptime .wrap("rowspan"), .wrap(str), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(TableCell); @@ -25,4 +47,12 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const colSpan = bridge.accessor(TableCell.getColSpan, TableCell.setColSpan, .{}); + pub const rowSpan = bridge.accessor(TableCell.getRowSpan, TableCell.setRowSpan, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.TableCell" { + try testing.htmlRunner("element/html/tablecell.html", .{}); +} diff --git a/src/browser/webapi/element/html/Time.zig b/src/browser/webapi/element/html/Time.zig index 3f3b1ef1..eb61d955 100644 --- a/src/browser/webapi/element/html/Time.zig +++ b/src/browser/webapi/element/html/Time.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,14 @@ pub fn asNode(self: *Time) *Node { return self.asElement().asNode(); } +pub fn getDateTime(self: *Time) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("datetime")) orelse ""; +} + +pub fn setDateTime(self: *Time, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("datetime"), .wrap(value), page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Time); @@ -22,4 +31,11 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const dateTime = bridge.accessor(Time.getDateTime, Time.setDateTime, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Time" { + try testing.htmlRunner("element/html/time.html", .{}); +} From eaf95a85a897572085e9d5cfb595f10833d1cf91 Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 12:00:44 +0100 Subject: [PATCH 2/2] fix OL.type default and TableCell span clamping per spec OL.type returns "1" (not "") when attribute absent. TableCell.colSpan clamps to 1-1000, rowSpan to 0-65534. --- src/browser/tests/element/html/ol.html | 2 +- src/browser/tests/element/html/tablecell.html | 16 ++++++++++++++++ src/browser/webapi/element/html/OL.zig | 2 +- src/browser/webapi/element/html/TableCell.zig | 7 +++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/browser/tests/element/html/ol.html b/src/browser/tests/element/html/ol.html index 8036e470..f999f80f 100644 --- a/src/browser/tests/element/html/ol.html +++ b/src/browser/tests/element/html/ol.html @@ -46,6 +46,6 @@ testing.expectEqual('1', ol1.type); const ol2 = document.getElementById('ol2'); - testing.expectEqual('', ol2.type); + testing.expectEqual('1', ol2.type); } diff --git a/src/browser/tests/element/html/tablecell.html b/src/browser/tests/element/html/tablecell.html index d1667288..5070ceeb 100644 --- a/src/browser/tests/element/html/tablecell.html +++ b/src/browser/tests/element/html/tablecell.html @@ -18,6 +18,14 @@ const td2 = document.getElementById('td2'); testing.expectEqual(1, td2.colSpan); + + // colSpan 0 clamps to 1 + td2.colSpan = 0; + testing.expectEqual(1, td2.colSpan); + + // colSpan > 1000 clamps to 1000 + td2.colSpan = 9999; + testing.expectEqual(1000, td2.colSpan); } @@ -31,5 +39,13 @@ const td2 = document.getElementById('td2'); testing.expectEqual(1, td2.rowSpan); + + // rowSpan 0 is valid per spec (span remaining rows) + td2.rowSpan = 0; + testing.expectEqual(0, td2.rowSpan); + + // rowSpan > 65534 clamps to 65534 + td2.rowSpan = 99999; + testing.expectEqual(65534, td2.rowSpan); } diff --git a/src/browser/webapi/element/html/OL.zig b/src/browser/webapi/element/html/OL.zig index 7001ae24..78baf5a9 100644 --- a/src/browser/webapi/element/html/OL.zig +++ b/src/browser/webapi/element/html/OL.zig @@ -56,7 +56,7 @@ pub fn setReversed(self: *OL, value: bool, page: *Page) !void { } pub fn getType(self: *OL) []const u8 { - return self.asElement().getAttributeSafe(comptime .wrap("type")) orelse ""; + return self.asElement().getAttributeSafe(comptime .wrap("type")) orelse "1"; } pub fn setType(self: *OL, value: []const u8, page: *Page) !void { diff --git a/src/browser/webapi/element/html/TableCell.zig b/src/browser/webapi/element/html/TableCell.zig index b8ddccca..d57b70ff 100644 --- a/src/browser/webapi/element/html/TableCell.zig +++ b/src/browser/webapi/element/html/TableCell.zig @@ -21,7 +21,9 @@ pub fn asNode(self: *TableCell) *Node { pub fn getColSpan(self: *TableCell) u32 { const attr = self.asElement().getAttributeSafe(comptime .wrap("colspan")) orelse return 1; - return std.fmt.parseUnsigned(u32, attr, 10) catch 1; + const v = std.fmt.parseUnsigned(u32, attr, 10) catch return 1; + if (v == 0) return 1; + return @min(v, 1000); } pub fn setColSpan(self: *TableCell, value: u32, page: *Page) !void { @@ -31,7 +33,8 @@ pub fn setColSpan(self: *TableCell, value: u32, page: *Page) !void { pub fn getRowSpan(self: *TableCell) u32 { const attr = self.asElement().getAttributeSafe(comptime .wrap("rowspan")) orelse return 1; - return std.fmt.parseUnsigned(u32, attr, 10) catch 1; + const v = std.fmt.parseUnsigned(u32, attr, 10) catch return 1; + return @min(v, 65534); } pub fn setRowSpan(self: *TableCell, value: u32, page: *Page) !void {