From e3e3fc25fe6db2571f51d09b0da5da9c3dd7dc41 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Jun 2026 17:33:37 +0800 Subject: [PATCH] Various HTML attribute tweaks Aimed at improving WPT /html/dom/reflection-obsolete.html Goes from 923 to 2305 passing cases (the remaining failing cases are all for which we don't currently support) Add accessors to Directory, Font and FrameSet. Add HTMLMarqueeElement. Font setColor null -> "" Add new properties to Html (accessKey and autofocus) and improve tabIndex parsing. --- src/browser/Frame.zig | 4 +- src/browser/js/bridge.zig | 1 + src/browser/tests/element/html/font.html | 47 ++++++ .../tests/element/html/htmlelement-props.html | 28 ++++ src/browser/tests/element/html/marquee.html | 100 +++++++++++ src/browser/webapi/Element.zig | 3 + .../webapi/css/CSSStyleDeclaration.zig | 2 +- src/browser/webapi/element/Html.zig | 84 +++++++++- src/browser/webapi/element/html/Directory.zig | 15 ++ src/browser/webapi/element/html/Font.zig | 37 +++++ src/browser/webapi/element/html/FrameSet.zig | 20 +++ src/browser/webapi/element/html/Marquee.zig | 156 ++++++++++++++++++ 12 files changed, 486 insertions(+), 11 deletions(-) create mode 100644 src/browser/tests/element/html/font.html create mode 100644 src/browser/tests/element/html/marquee.html create mode 100644 src/browser/webapi/element/html/Marquee.zig diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 8f4876d2..ffafef0d 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -2586,10 +2586,10 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const .{ ._proto = undefined }, ), asUint("marquee") => return self.createHtmlElementT( - Element.Html.Generic, + Element.Html.Marquee, namespace, attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("marquee"), ._tag = .marquee }, + .{ ._proto = undefined }, ), asUint("address") => return self.createHtmlElementT( Element.Html.Generic, diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 2335d89a..ef215554 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -866,6 +866,7 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/element/html/LI.zig"), @import("../webapi/element/html/Link.zig"), @import("../webapi/element/html/Map.zig"), + @import("../webapi/element/html/Marquee.zig"), @import("../webapi/element/html/Media.zig"), @import("../webapi/element/html/Meta.zig"), @import("../webapi/element/html/Meter.zig"), diff --git a/src/browser/tests/element/html/font.html b/src/browser/tests/element/html/font.html new file mode 100644 index 00000000..bd3d6d31 --- /dev/null +++ b/src/browser/tests/element/html/font.html @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/src/browser/tests/element/html/htmlelement-props.html b/src/browser/tests/element/html/htmlelement-props.html index a008adc5..6e625d4f 100644 --- a/src/browser/tests/element/html/htmlelement-props.html +++ b/src/browser/tests/element/html/htmlelement-props.html @@ -52,5 +52,33 @@ const textarea = document.createElement('textarea'); testing.expectEqual(0, textarea.tabIndex); + + // tabIndex follows the HTML "rules for parsing integers": skip leading + // ASCII whitespace, take an optional sign and the leading run of digits, + // ignoring any trailing junk. + d3.setAttribute('tabindex', ' 7'); + testing.expectEqual(7, d3.tabIndex); + + d3.setAttribute('tabindex', '\t\n\f\r9'); + testing.expectEqual(9, d3.tabIndex); + + d3.setAttribute('tabindex', '7%'); + testing.expectEqual(7, d3.tabIndex); + + d3.setAttribute('tabindex', '1.5'); + testing.expectEqual(1, d3.tabIndex); + + d3.setAttribute('tabindex', '-3'); + testing.expectEqual(-3, d3.tabIndex); + + d3.setAttribute('tabindex', '+4'); + testing.expectEqual(4, d3.tabIndex); + + // Non-numeric values fall back to the element's default (-1 here). + d3.setAttribute('tabindex', 'foo'); + testing.expectEqual(-1, d3.tabIndex); + + d3.setAttribute('tabindex', ''); + testing.expectEqual(-1, d3.tabIndex); } diff --git a/src/browser/tests/element/html/marquee.html b/src/browser/tests/element/html/marquee.html new file mode 100644 index 00000000..12fb4d99 --- /dev/null +++ b/src/browser/tests/element/html/marquee.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 3a26798e..d898dbd7 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -233,6 +233,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .li => "li", .link => "link", .map => "map", + .marquee => "marquee", .media => |m| switch (m._type) { .audio => "audio", .video => "video", @@ -313,6 +314,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .li => "LI", .link => "LINK", .map => "MAP", + .marquee => "MARQUEE", .meta => "META", .media => |m| switch (m._type) { .audio => "AUDIO", @@ -1507,6 +1509,7 @@ pub fn getTag(self: *const Element) Tag { .legend => .legend, .li => .li, .map => .map, + .marquee => .marquee, .ul => .ul, .ol => .ol, .object => .object, diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 323bf3af..12833c41 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -722,7 +722,7 @@ fn getDefaultDisplay(element: *const Element) []const u8 { .html => |html| { return switch (html._type) { .anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline", - .body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block", + .body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .marquee, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block", .generic, .custom, .unknown, .data => blk: { const tag = element.getTagNameLower(); if (isInlineTag(tag)) break :blk "inline"; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9a0a9f0c..96a49cfc 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -61,6 +61,7 @@ pub const Legend = @import("html/Legend.zig"); pub const LI = @import("html/LI.zig"); pub const Link = @import("html/Link.zig"); pub const Map = @import("html/Map.zig"); +pub const Marquee = @import("html/Marquee.zig"); pub const Media = @import("html/Media.zig"); pub const Meta = @import("html/Meta.zig"); pub const Meter = @import("html/Meter.zig"); @@ -151,6 +152,7 @@ pub const Type = union(enum) { li: *LI, link: *Link, map: *Map, + marquee: *Marquee, media: *Media, meta: *Meta, meter: *Meter, @@ -342,14 +344,50 @@ pub fn setHidden(self: *HtmlElement, hidden: bool, frame: *Frame) !void { } pub fn getTabIndex(self: *HtmlElement) i32 { - const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse { - // Per spec, interactive/focusable elements default to 0 when tabindex is absent - return switch (self._type) { - .anchor, .area, .button, .input, .select, .textarea, .iframe => 0, - else => -1, - }; + const default: i32 = switch (self._type) { + .anchor, .area, .button, .input, .select, .textarea, .iframe => 0, + else => -1, }; - return std.fmt.parseInt(i32, attr, 10) catch -1; + const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse return default; + return parseInteger(attr) orelse default; +} + +// HTML integer parsing is lax +pub fn parseInteger(input: []const u8) ?i32 { + var normalized = std.mem.trimStart(u8, input, "\t\n\r\x0c "); + if (normalized.len == 0) { + return null; + } + + var negative = false; + if (normalized[0] == '-') { + negative = true; + normalized = normalized[1..]; + } else if (normalized[0] == '+') { + normalized = normalized[1..]; + } + + if (normalized.len == 0 or std.ascii.isDigit(normalized[0]) == false) { + return null; + } + + var i: usize = 0; + var value: i64 = 0; + while (i < normalized.len and std.ascii.isDigit(normalized[i])) : (i += 1) { + value = value * 10 + (normalized[i] - '0'); + if (value > 2147483648) { + return null; + } + } + + if (negative) { + value = -value; + } + + if (value < -2147483648 or value > 2147483647) { + return null; + } + return @intCast(value); } pub fn setTabIndex(self: *HtmlElement, value: i32, frame: *Frame) !void { @@ -359,13 +397,41 @@ pub fn setTabIndex(self: *HtmlElement, value: i32, frame: *Frame) !void { } pub fn getDir(self: *HtmlElement) []const u8 { - return self.asElement().getAttributeSafe(comptime .wrap("dir")) orelse ""; + // `dir` reflects as a "limited to only known values" enumerated + // attribute: the getter returns the canonical (lowercase) keyword only + // when the content attribute matches one ASCII-case-insensitively, + // otherwise the empty string. + const attr = self.asElement().getAttributeSafe(comptime .wrap("dir")) orelse return ""; + inline for (.{ "ltr", "rtl", "auto" }) |keyword| { + if (std.ascii.eqlIgnoreCase(attr, keyword)) return keyword; + } + return ""; } pub fn setDir(self: *HtmlElement, value: []const u8, frame: *Frame) !void { try self.asElement().setAttributeSafe(comptime .wrap("dir"), .wrap(value), frame); } +pub fn getAccessKey(self: *HtmlElement) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("accesskey")) orelse ""; +} + +pub fn setAccessKey(self: *HtmlElement, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("accesskey"), .wrap(value), frame); +} + +pub fn getAutofocus(self: *HtmlElement) bool { + return self.asElement().getAttributeSafe(comptime .wrap("autofocus")) != null; +} + +pub fn setAutofocus(self: *HtmlElement, autofocus: bool, frame: *Frame) !void { + if (autofocus) { + try self.asElement().setAttributeSafe(comptime .wrap("autofocus"), .wrap(""), frame); + } else { + try self.asElement().removeAttribute(comptime .wrap("autofocus"), frame); + } +} + pub fn getLang(self: *HtmlElement) []const u8 { return self.asElement().getAttributeSafe(comptime .wrap("lang")) orelse ""; } @@ -1243,6 +1309,8 @@ pub const JsApi = struct { pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true }); pub const click = bridge.function(HtmlElement.click, .{}); + pub const accessKey = bridge.accessor(HtmlElement.getAccessKey, HtmlElement.setAccessKey, .{ .ce_reactions = true }); + pub const autofocus = bridge.accessor(HtmlElement.getAutofocus, HtmlElement.setAutofocus, .{ .ce_reactions = true }); pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{ .ce_reactions = true }); pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true }); pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{}); diff --git a/src/browser/webapi/element/html/Directory.zig b/src/browser/webapi/element/html/Directory.zig index cd9f44d0..e0c488e7 100644 --- a/src/browser/webapi/element/html/Directory.zig +++ b/src/browser/webapi/element/html/Directory.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,18 @@ pub fn asNode(self: *Directory) *Node { return self.asElement().asNode(); } +pub fn getCompact(self: *Directory) bool { + return self.asElement().getAttributeSafe(comptime .wrap("compact")) != null; +} + +pub fn setCompact(self: *Directory, compact: bool, frame: *Frame) !void { + if (compact) { + try self.asElement().setAttributeSafe(comptime .wrap("compact"), .wrap(""), frame); + } else { + try self.asElement().removeAttribute(comptime .wrap("compact"), frame); + } +} + pub const JsApi = struct { pub const bridge = js.Bridge(Directory); @@ -22,4 +35,6 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const compact = bridge.accessor(Directory.getCompact, Directory.setCompact, .{ .ce_reactions = true }); }; diff --git a/src/browser/webapi/element/html/Font.zig b/src/browser/webapi/element/html/Font.zig index 3c6dde77..fe0a7bea 100644 --- a/src/browser/webapi/element/html/Font.zig +++ b/src/browser/webapi/element/html/Font.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,33 @@ pub fn asNode(self: *Font) *Node { return self.asElement().asNode(); } +pub fn getColor(self: *Font) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("color")) orelse ""; +} + +pub fn setColor(self: *Font, value: js.Value, frame: *Frame) !void { + // color is `[LegacyNullToEmptyString] DOMString`: a JS null becomes "", + // not the string "null". + const str: []const u8 = if (value.isNull()) "" else try value.toZig([]const u8); + try self.asElement().setAttributeSafe(comptime .wrap("color"), .wrap(str), frame); +} + +pub fn getFace(self: *Font) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("face")) orelse ""; +} + +pub fn setFace(self: *Font, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("face"), .wrap(value), frame); +} + +pub fn getSize(self: *Font) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("size")) orelse ""; +} + +pub fn setSize(self: *Font, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("size"), .wrap(value), frame); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Font); @@ -22,4 +50,13 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const color = bridge.accessor(Font.getColor, Font.setColor, .{ .ce_reactions = true }); + pub const face = bridge.accessor(Font.getFace, Font.setFace, .{ .ce_reactions = true }); + pub const size = bridge.accessor(Font.getSize, Font.setSize, .{ .ce_reactions = true }); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Font" { + try testing.htmlRunner("element/html/font.html", .{}); +} diff --git a/src/browser/webapi/element/html/FrameSet.zig b/src/browser/webapi/element/html/FrameSet.zig index cafec13d..4e84a25c 100644 --- a/src/browser/webapi/element/html/FrameSet.zig +++ b/src/browser/webapi/element/html/FrameSet.zig @@ -1,4 +1,5 @@ const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +15,22 @@ pub fn asNode(self: *FrameSet) *Node { return self.asElement().asNode(); } +pub fn getCols(self: *FrameSet) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("cols")) orelse ""; +} + +pub fn setCols(self: *FrameSet, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("cols"), .wrap(value), frame); +} + +pub fn getRows(self: *FrameSet) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("rows")) orelse ""; +} + +pub fn setRows(self: *FrameSet, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("rows"), .wrap(value), frame); +} + pub const JsApi = struct { pub const bridge = js.Bridge(FrameSet); @@ -22,6 +39,9 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const cols = bridge.accessor(FrameSet.getCols, FrameSet.setCols, .{ .ce_reactions = true }); + pub const rows = bridge.accessor(FrameSet.getRows, FrameSet.setRows, .{ .ce_reactions = true }); }; const testing = @import("../../../../testing.zig"); diff --git a/src/browser/webapi/element/html/Marquee.zig b/src/browser/webapi/element/html/Marquee.zig new file mode 100644 index 00000000..4d0c4950 --- /dev/null +++ b/src/browser/webapi/element/html/Marquee.zig @@ -0,0 +1,156 @@ +const std = @import("std"); + +const js = @import("../../../js/js.zig"); +const Frame = @import("../../../Frame.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); + +const Marquee = @This(); + +_proto: *HtmlElement, + +pub fn asElement(self: *Marquee) *Element { + return self._proto._proto; +} +pub fn asNode(self: *Marquee) *Node { + return self.asElement().asNode(); +} + +pub fn getBehavior(self: *Marquee) []const u8 { + return getEnumerated(self, "behavior", &.{ "scroll", "slide", "alternate" }, "scroll"); +} + +pub fn setBehavior(self: *Marquee, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("behavior"), .wrap(value), frame); +} + +pub fn getDirection(self: *Marquee) []const u8 { + return getEnumerated(self, "direction", &.{ "up", "right", "down", "left" }, "left"); +} + +pub fn setDirection(self: *Marquee, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("direction"), .wrap(value), frame); +} + +pub fn getBgColor(self: *Marquee) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("bgcolor")) orelse ""; +} + +pub fn setBgColor(self: *Marquee, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("bgcolor"), .wrap(value), frame); +} + +pub fn getHeight(self: *Marquee) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("height")) orelse ""; +} + +pub fn setHeight(self: *Marquee, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(value), frame); +} + +pub fn getWidth(self: *Marquee) []const u8 { + return self.asElement().getAttributeSafe(comptime .wrap("width")) orelse ""; +} + +pub fn setWidth(self: *Marquee, value: []const u8, frame: *Frame) !void { + try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(value), frame); +} + +pub fn getHspace(self: *Marquee) u32 { + return getU32(self, "hspace", 0); +} + +pub fn setHspace(self: *Marquee, value: u32, frame: *Frame) !void { + try setU32(self, "hspace", value, 0, frame); +} + +pub fn getVspace(self: *Marquee) u32 { + return getU32(self, "vspace", 0); +} + +pub fn setVspace(self: *Marquee, value: u32, frame: *Frame) !void { + try setU32(self, "vspace", value, 0, frame); +} + +pub fn getScrollAmount(self: *Marquee) u32 { + return getU32(self, "scrollamount", 6); +} + +pub fn setScrollAmount(self: *Marquee, value: u32, frame: *Frame) !void { + try setU32(self, "scrollamount", value, 6, frame); +} + +pub fn getScrollDelay(self: *Marquee) u32 { + return getU32(self, "scrolldelay", 85); +} + +pub fn setScrollDelay(self: *Marquee, value: u32, frame: *Frame) !void { + try setU32(self, "scrolldelay", value, 85, frame); +} + +pub fn getTrueSpeed(self: *Marquee) bool { + return self.asElement().getAttributeSafe(comptime .wrap("truespeed")) != null; +} + +pub fn setTrueSpeed(self: *Marquee, truespeed: bool, frame: *Frame) !void { + if (truespeed) { + try self.asElement().setAttributeSafe(comptime .wrap("truespeed"), .wrap(""), frame); + } else { + try self.asElement().removeAttribute(comptime .wrap("truespeed"), frame); + } +} + +fn getEnumerated(self: *Marquee, comptime attr: []const u8, keywords: []const []const u8, default: []const u8) []const u8 { + const value = self.asElement().getAttributeSafe(comptime .wrap(attr)) orelse return default; + for (keywords) |keyword| { + if (std.ascii.eqlIgnoreCase(value, keyword)) return keyword; + } + return default; +} + +// Reflects an `unsigned long` content attribute: parses with the "rules for +// parsing non-negative integers" (the lax integer parser, then rejecting a +// negative result), so a valid value lands in [0, 2147483647]. +fn getU32(self: *Marquee, comptime attr: []const u8, default: u32) u32 { + const value = self.asElement().getAttributeSafe(comptime .wrap(attr)) orelse return default; + const parsed = HtmlElement.parseInteger(value) orelse return default; + + if (parsed < 0) { + return default; + } + return @intCast(parsed); +} + +fn setU32(self: *Marquee, comptime attr: []const u8, value: u32, default: u32, frame: *Frame) !void { + const written = if (value > 2147483647) default else value; + var buf: [10]u8 = undefined; + const str = std.fmt.bufPrint(&buf, "{d}", .{written}) catch unreachable; + try self.asElement().setAttributeSafe(comptime .wrap(attr), .wrap(str), frame); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Marquee); + + pub const Meta = struct { + pub const name = "HTMLMarqueeElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const behavior = bridge.accessor(Marquee.getBehavior, Marquee.setBehavior, .{ .ce_reactions = true }); + pub const bgColor = bridge.accessor(Marquee.getBgColor, Marquee.setBgColor, .{ .ce_reactions = true }); + pub const direction = bridge.accessor(Marquee.getDirection, Marquee.setDirection, .{ .ce_reactions = true }); + pub const height = bridge.accessor(Marquee.getHeight, Marquee.setHeight, .{ .ce_reactions = true }); + pub const hspace = bridge.accessor(Marquee.getHspace, Marquee.setHspace, .{ .ce_reactions = true }); + pub const scrollAmount = bridge.accessor(Marquee.getScrollAmount, Marquee.setScrollAmount, .{ .ce_reactions = true }); + pub const scrollDelay = bridge.accessor(Marquee.getScrollDelay, Marquee.setScrollDelay, .{ .ce_reactions = true }); + pub const trueSpeed = bridge.accessor(Marquee.getTrueSpeed, Marquee.setTrueSpeed, .{ .ce_reactions = true }); + pub const vspace = bridge.accessor(Marquee.getVspace, Marquee.setVspace, .{ .ce_reactions = true }); + pub const width = bridge.accessor(Marquee.getWidth, Marquee.setWidth, .{ .ce_reactions = true }); +}; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Marquee" { + try testing.htmlRunner("element/html/marquee.html", .{}); +}