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", .{}); +}