Merge pull request #1581 from egrs/element-attribute-reflections

add attribute reflections for 8 HTML element types
This commit is contained in:
Karl Seguin
2026-02-18 19:50:23 +08:00
committed by GitHub
16 changed files with 454 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<fieldset id="fs1" disabled name="group1">
<input type="text">
</fieldset>
<fieldset id="fs2">
<input type="text">
</fieldset>
<script id="disabled">
{
const fs1 = document.getElementById('fs1');
testing.expectEqual(true, fs1.disabled);
fs1.disabled = false;
testing.expectEqual(false, fs1.disabled);
const fs2 = document.getElementById('fs2');
testing.expectEqual(false, fs2.disabled);
}
</script>
<script id="name">
{
const fs1 = document.getElementById('fs1');
testing.expectEqual('group1', fs1.name);
fs1.name = 'updated';
testing.expectEqual('updated', fs1.name);
const fs2 = document.getElementById('fs2');
testing.expectEqual('', fs2.name);
}
</script>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<label id="l1" for="input1">Name</label>
<input id="input1">
<script id="htmlFor">
{
const l1 = document.getElementById('l1');
testing.expectEqual('input1', l1.htmlFor);
l1.htmlFor = 'input2';
testing.expectEqual('input2', l1.htmlFor);
const l2 = document.createElement('label');
testing.expectEqual('', l2.htmlFor);
}
</script>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<ol>
<li id="li1" value="5">Item</li>
<li id="li2">Item</li>
</ol>
<script id="value">
{
const li1 = document.getElementById('li1');
testing.expectEqual(5, li1.value);
li1.value = 10;
testing.expectEqual(10, li1.value);
const li2 = document.getElementById('li2');
testing.expectEqual(0, li2.value);
li2.value = -3;
testing.expectEqual(-3, li2.value);
}
</script>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<ol id="ol1" start="5" reversed type="a">
<li>Item</li>
</ol>
<ol id="ol2">
<li>Item</li>
</ol>
<script id="start">
{
const ol1 = document.getElementById('ol1');
testing.expectEqual(5, ol1.start);
ol1.start = 10;
testing.expectEqual(10, ol1.start);
const ol2 = document.getElementById('ol2');
testing.expectEqual(1, ol2.start);
}
</script>
<script id="reversed">
{
const ol1 = document.getElementById('ol1');
testing.expectEqual(true, ol1.reversed);
ol1.reversed = false;
testing.expectEqual(false, ol1.reversed);
const ol2 = document.getElementById('ol2');
testing.expectEqual(false, ol2.reversed);
ol2.reversed = true;
testing.expectEqual(true, ol2.reversed);
}
</script>
<script id="type">
{
const ol1 = document.getElementById('ol1');
testing.expectEqual('a', ol1.type);
ol1.type = '1';
testing.expectEqual('1', ol1.type);
const ol2 = document.getElementById('ol2');
testing.expectEqual('1', ol2.type);
}
</script>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<select>
<optgroup id="og1" label="Group 1" disabled>
<option>A</option>
</optgroup>
<optgroup id="og2" label="Group 2">
<option>B</option>
</optgroup>
</select>
<script id="disabled">
{
const og1 = document.getElementById('og1');
testing.expectEqual(true, og1.disabled);
og1.disabled = false;
testing.expectEqual(false, og1.disabled);
const og2 = document.getElementById('og2');
testing.expectEqual(false, og2.disabled);
og2.disabled = true;
testing.expectEqual(true, og2.disabled);
}
</script>
<script id="label">
{
const og1 = document.getElementById('og1');
testing.expectEqual('Group 1', og1.label);
og1.label = 'Updated';
testing.expectEqual('Updated', og1.label);
const og = document.createElement('optgroup');
testing.expectEqual('', og.label);
}
</script>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<blockquote id="q1" cite="https://example.com/source">Quote</blockquote>
<script id="cite">
{
const q1 = document.getElementById('q1');
testing.expectEqual('https://example.com/source', q1.cite);
q1.cite = 'https://example.com/other';
testing.expectEqual('https://example.com/other', q1.cite);
const q2 = document.createElement('blockquote');
testing.expectEqual('', q2.cite);
}
</script>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<table>
<tr>
<td id="td1" colspan="3" rowspan="2">Cell</td>
<td id="td2">Cell</td>
</tr>
</table>
<script id="colSpan">
{
const td1 = document.getElementById('td1');
testing.expectEqual(3, td1.colSpan);
td1.colSpan = 5;
testing.expectEqual(5, td1.colSpan);
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);
}
</script>
<script id="rowSpan">
{
const td1 = document.getElementById('td1');
testing.expectEqual(2, td1.rowSpan);
td1.rowSpan = 4;
testing.expectEqual(4, td1.rowSpan);
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);
}
</script>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<time id="t1" datetime="2024-01-15">January 15</time>
<script id="dateTime">
{
const t = document.getElementById('t1');
testing.expectEqual('2024-01-15', t.dateTime);
t.dateTime = '2024-12-25T10:00';
testing.expectEqual('2024-12-25T10:00', t.dateTime);
const t2 = document.createElement('time');
testing.expectEqual('', t2.dateTime);
}
</script>

View File

@@ -1,4 +1,5 @@
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -14,6 +15,26 @@ pub fn asNode(self: *FieldSet) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(FieldSet); pub const bridge = js.Bridge(FieldSet);
@@ -22,4 +43,12 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -16,7 +16,9 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -31,6 +33,16 @@ pub fn asNode(self: *LI) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(LI); pub const bridge = js.Bridge(LI);
@@ -39,4 +51,11 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -1,4 +1,5 @@
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -14,6 +15,14 @@ pub fn asNode(self: *Label) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(Label); pub const bridge = js.Bridge(Label);
@@ -22,4 +31,11 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -16,7 +16,9 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -31,6 +33,36 @@ pub fn asNode(self: *OL) *Node {
return self.asElement().asNode(); 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 "1";
}
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 JsApi = struct {
pub const bridge = js.Bridge(OL); pub const bridge = js.Bridge(OL);
@@ -39,4 +71,13 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -1,4 +1,5 @@
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -14,6 +15,26 @@ pub fn asNode(self: *OptGroup) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(OptGroup); pub const bridge = js.Bridge(OptGroup);
@@ -22,4 +43,12 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -1,5 +1,6 @@
const String = @import("../../../../string.zig").String; const String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -17,6 +18,17 @@ pub fn asNode(self: *Quote) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(Quote); pub const bridge = js.Bridge(Quote);
@@ -25,4 +37,11 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -1,5 +1,7 @@
const std = @import("std");
const String = @import("../../../../string.zig").String; const String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -17,6 +19,29 @@ pub fn asNode(self: *TableCell) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
pub fn getColSpan(self: *TableCell) u32 {
const attr = self.asElement().getAttributeSafe(comptime .wrap("colspan")) orelse return 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 {
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;
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 {
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 JsApi = struct {
pub const bridge = js.Bridge(TableCell); pub const bridge = js.Bridge(TableCell);
@@ -25,4 +50,12 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}

View File

@@ -1,4 +1,5 @@
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
@@ -14,6 +15,14 @@ pub fn asNode(self: *Time) *Node {
return self.asElement().asNode(); 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 JsApi = struct {
pub const bridge = js.Bridge(Time); pub const bridge = js.Bridge(Time);
@@ -22,4 +31,11 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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", .{});
}