diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index e55af9316..3631ffb8f 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -134,6 +134,7 @@ _element_computed_styles: Element.StyleLookup = .empty, _element_datasets: Element.DatasetLookup = .empty, _element_class_lists: Element.ClassListLookup = .empty, _element_rel_lists: Element.RelListLookup = .empty, +_element_token_lists: Element.TokenListLookup = .empty, _element_shadow_roots: Element.ShadowRootLookup = .empty, _node_owner_documents: Node.OwnerDocumentLookup = .empty, _element_scroll_positions: Element.ScrollPositionLookup = .empty, diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index b24340c9b..192537f32 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -742,6 +742,14 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T { // Extracted so that it can be used in both jsValueToZig and in // probeJsValueToZig. Avoids having to duplicate this logic when probing. fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T { + // js.Nullable(T): a required argument that accepts null. + if (@hasDecl(T, "js_nullable")) { + if (js_val.isNullOrUndefined()) { + return T{ .value = null }; + } + return T{ .value = try self.jsValueToZig(T.js_nullable, js_val) }; + } + return switch (T) { js.Function, js.Function.Global => { if (!js_val.isFunction()) { diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 675f6955b..a8bec4d91 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -284,6 +284,18 @@ pub const NullableString = struct { value: []const u8, }; +// A required argument that accepts null (Web IDL "T?"): unlike a Zig optional +// parameter, omitting the argument is a TypeError, while passing null or +// undefined yields .{ .value = null }. It also counts towards the JS-visible +// function length, which a plain optional would not. +pub fn Nullable(comptime T: type) type { + return struct { + value: ?T, + + pub const js_nullable = T; + }; +} + pub const Exception = struct { local: *const Local, handle: *const v8.Value, diff --git a/src/browser/tests/document/create_element_ns.html b/src/browser/tests/document/create_element_ns.html index c99a51674..095025dce 100644 --- a/src/browser/tests/document/create_element_ns.html +++ b/src/browser/tests/document/create_element_ns.html @@ -32,8 +32,7 @@ const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom'); testing.expectEqual('custom', unknownNsElement.tagName); - // Should be http://example.com/unknown - testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI); + testing.expectEqual('http://example.com/unknown', unknownNsElement.namespaceURI); const regularDiv = document.createElement('div'); testing.expectEqual('DIV', regularDiv.tagName); @@ -45,5 +44,5 @@ testing.expectEqual('te:ST', custom.tagName); testing.expectEqual('te', custom.prefix); testing.expectEqual('ST', custom.localName); - testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test + testing.expectEqual('test', custom.namespaceURI); diff --git a/src/browser/tests/domimplementation.html b/src/browser/tests/domimplementation.html index 76b1a50fd..295ea8a61 100644 --- a/src/browser/tests/domimplementation.html +++ b/src/browser/tests/domimplementation.html @@ -221,8 +221,7 @@ const root = doc.documentElement; testing.expectEqual('prefix:localName', root.tagName); - // TODO: Custom namespaces are being replaced with an empty value - testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI); + testing.expectEqual('http://example.com', root.namespaceURI); } diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index b9e34faec..5ad2f9758 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -167,7 +167,12 @@ pub fn createAborted(reason_: ?js.Value, exec: *const Execution) !*AbortSignal { return signal; } -pub fn createAny(signals: []const *AbortSignal, exec: *const Execution) !*AbortSignal { +pub fn createAny(signals_value: js.Value, exec: *const Execution) !*AbortSignal { + // The parameter isn't optional. If we declared it as a slice directy, the + // bridge would treat it as a variadic and map it to empty rather than throwing + // a TypeError as it should. + const signals = try signals_value.toZig([]const *AbortSignal); + const result = try init(exec); for (signals) |source| { if (source._aborted) { @@ -279,7 +284,6 @@ pub const JsApi = struct { pub const Prototype = EventTarget; - pub const constructor = bridge.constructor(AbortSignal.init, .{}); pub const aborted = bridge.accessor(AbortSignal.getAborted, null, .{}); pub const reason = bridge.accessor(AbortSignal.getReason, null, .{}); pub const onabort = bridge.accessor(AbortSignal.getOnAbort, AbortSignal.setOnAbort, .{}); diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 21e30804c..1e0a44b69 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -16,6 +16,7 @@ // 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 Frame = @import("../Frame.zig"); const Node = @import("Node.zig"); @@ -23,10 +24,26 @@ const Document = @import("Document.zig"); const DocumentType = @import("DocumentType.zig"); const DOMImplementation = @This(); -_pad: bool = false, -pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType { - return DocumentType.init(qualified_name, public_id, system_id, frame); +// The document this implementation object belongs to: nodes created through +// it are owned by that document, not necessarily the frame's main document. +_document: *Document, + +pub fn createDocumentType(self: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType { + // Per spec, qualifiedName must match the doctype name production: any + // characters except ASCII whitespace or '>'. + for (qualified_name) |c| { + switch (c) { + '\t', '\n', 0x0C, '\r', ' ', '>' => return error.InvalidCharacterError, + else => {}, + } + } + + const doctype = try DocumentType.init(qualified_name, public_id, system_id, frame); + if (self._document != frame.document) { + try frame.setNodeOwnerDocument(doctype.asNode(), self._document); + } + return doctype; } pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, frame: *Frame) !*Document { @@ -63,10 +80,34 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString return document; } -pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, frame: *Frame) !*Document { +pub fn createDocument(_: *const DOMImplementation, namespace_nullable: js.Nullable([]const u8), qualified_name_: js.Value, doctype: ?*DocumentType, frame: *Frame) !*Document { + // Both namespace (nullable) and qualifiedName are required arguments. + const namespace_ = namespace_nullable.value; + + // Per Web IDL, qualifiedName is [LegacyNullToEmptyString]: null becomes + // the empty string, while undefined stringifies to "undefined". The raw + // js.Value keeps that distinction. + const qname: []const u8 = blk: { + if (qualified_name_.isNull()) { + break :blk ""; + } + break :blk try qualified_name_.toStringSlice(); + }; + + if (qname.len > 0) { + _ = try Document.validateAndExtract(namespace_, qname, .element); + } + // Create XML Document const document = (try frame._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument(); document._url = "about:blank"; + // Per spec the content type depends on the requested namespace. + document._content_type = blk: { + const ns = namespace_ orelse break :blk "application/xml"; + if (std.mem.eql(u8, ns, "http://www.w3.org/1999/xhtml")) break :blk "application/xhtml+xml"; + if (std.mem.eql(u8, ns, "http://www.w3.org/2000/svg")) break :blk "image/svg+xml"; + break :blk "application/xml"; + }; // Append doctype if provided if (doctype) |dt| { @@ -74,12 +115,20 @@ pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qual } // Create and append root element if qualified_name provided - if (qualified_name) |qname| { - if (qname.len > 0) { - const namespace = Node.Element.Namespace.parse(namespace_); - const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null); - _ = try document.asNode().appendChild(root, frame); + if (qname.len > 0) { + const namespace = Node.Element.Namespace.parse(namespace_); + const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null); + + // Store the original URI for unknown namespaces so namespaceURI and + // lookupNamespaceURI can return it (mirrors Document.createElementNS). + if (namespace == .unknown) { + if (namespace_) |uri| { + const duped = try frame.dupeString(uri); + try frame._element_namespace_uris.put(frame.arena, root.as(Node.Element), duped); + } } + + _ = try document.asNode().appendChild(root, frame); } return document; @@ -98,7 +147,6 @@ pub const JsApi = struct { pub const name = "DOMImplementation"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const empty_with_no_proto = true; }; pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{}); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 10548f3b8..c0b270ff8 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -54,6 +54,8 @@ _type: Type, _proto: *Node, _frame: ?*Frame = null, _url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank) +// content type override for documents created via DOMImplementation.createDocument +_content_type: ?[]const u8 = null, _ready_state: ReadyState = .loading, _current_script: ?*Element.Html.Script = null, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, @@ -158,6 +160,9 @@ pub fn setLocation(self: *Document, url: [:0]const u8) !void { } pub fn getContentType(self: *const Document) []const u8 { + if (self._content_type) |content_type| { + return content_type; + } return switch (self._type) { .html => "text/html", .xml => "application/xml", @@ -300,7 +305,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement } pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, frame: *Frame) !*Element { - try validateElementName(name); + _ = try validateAndExtract(namespace, name, .element); const ns = Element.Namespace.parse(namespace); // Per spec, createElementNS does NOT lowercase (unlike createElement). const node = try Frame.node_factory.createElementNS(frame, ns, name, null); @@ -419,7 +424,7 @@ pub fn querySelectorAll(self: *Document, input: String, frame: *Frame) !*Selecto pub fn getImplementation(self: *Document, frame: *Frame) !*DOMImplementation { if (self._implementation) |impl| return impl; - const impl = try frame._factory.create(DOMImplementation{}); + const impl = try frame._factory.create(DOMImplementation{ ._document = self }); self._implementation = impl; return impl; } @@ -1239,26 +1244,118 @@ fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, compti } } -fn validateElementName(name: []const u8) !void { +// DOM ยง1.4 "Name validation" productions. + +pub fn isValidElementLocalName(name: []const u8) bool { if (name.len == 0) { - return error.InvalidCharacterError; + return false; } - - const first = name[0]; - // Element names cannot start with: digits, period, hyphen - if ((first >= '0' and first <= '9') or first == '.' or first == '-') { - return error.InvalidCharacterError; + if (std.ascii.isAlphabetic(name[0])) { + // Names the HTML parser can construct: anything except ASCII + // whitespace, NUL, '/' or '>'. + for (name[1..]) |c| { + switch (c) { + '\t', '\n', 0x0C, '\r', ' ', 0, '/', '>' => return false, + else => {}, + } + } + return true; + } + // Otherwise the first code point must be ':', '_' or beyond ASCII, and + // the rest restricted to alphanumerics, '-', '.', ':', '_' or non-ASCII. + if (name[0] != ':' and name[0] != '_' and name[0] < 0x80) { + return false; } - for (name[1..]) |c| { - const is_valid = std.ascii.isAlphanumeric(c) or - c == '_' or c == '-' or c == '.' or c == ':' or - c >= 128; // Allow non-ASCII UTF-8 + const valid = std.ascii.isAlphanumeric(c) or + c == '-' or c == '.' or c == ':' or c == '_' or c >= 0x80; + if (!valid) { + return false; + } + } + return true; +} - if (!is_valid) { +pub fn isValidNamespacePrefix(prefix: []const u8) bool { + if (prefix.len == 0) { + return false; + } + for (prefix) |c| { + switch (c) { + '\t', '\n', 0x0C, '\r', ' ', 0, '/', '>' => return false, + else => {}, + } + } + return true; +} + +pub fn isValidAttributeLocalName(name: []const u8) bool { + if (name.len == 0) { + return false; + } + for (name) |c| { + switch (c) { + '\t', '\n', 0x0C, '\r', ' ', 0, '/', '=', '>' => return false, + else => {}, + } + } + return true; +} + +fn validateElementName(name: []const u8) !void { + if (!isValidElementLocalName(name)) { + return error.InvalidCharacterError; + } +} + +pub const ValidatedName = struct { + prefix: ?[]const u8, + local_name: []const u8, + namespace: ?[]const u8, +}; + +// The DOM spec's "validate and extract a namespace and qualifiedName". +pub fn validateAndExtract(namespace_: ?[]const u8, qualified_name: []const u8, comptime context: enum { element, attribute }) !ValidatedName { + var namespace: ?[]const u8 = namespace_; + if (namespace) |ns| { + if (ns.len == 0) { + namespace = null; + } + } + + var prefix: ?[]const u8 = null; + var local_name = qualified_name; + if (std.mem.indexOfScalar(u8, qualified_name, ':')) |colon| { + prefix = qualified_name[0..colon]; + local_name = qualified_name[colon + 1 ..]; + if (!isValidNamespacePrefix(prefix.?)) { return error.InvalidCharacterError; } } + + const local_valid = switch (context) { + .element => isValidElementLocalName(local_name), + .attribute => isValidAttributeLocalName(local_name), + }; + if (!local_valid) { + return error.InvalidCharacterError; + } + + if (prefix != null and namespace == null) { + return error.NamespaceError; + } + if (prefix) |p| { + if (std.mem.eql(u8, p, "xml") and (namespace == null or !std.mem.eql(u8, namespace.?, "http://www.w3.org/XML/1998/namespace"))) { + return error.NamespaceError; + } + } + const is_xmlns = std.mem.eql(u8, qualified_name, "xmlns") or (prefix != null and std.mem.eql(u8, prefix.?, "xmlns")); + const ns_is_xmlns = namespace != null and std.mem.eql(u8, namespace.?, "http://www.w3.org/2000/xmlns/"); + if (is_xmlns != ns_is_xmlns) { + return error.NamespaceError; + } + + return .{ .prefix = prefix, .local_name = local_name, .namespace = namespace }; } // When a frame's URL is about:blank, or as soon as a frame is diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 42150597b..dc26368a8 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -885,6 +885,23 @@ pub fn getRelList(self: *Element, frame: *Frame) !*collections.DOMTokenList { return gop.value_ptr.*; } +// The other DOMTokenList-reflected attributes (class and rel have dedicated +// lookups above). +pub const TokenListAttribute = enum { sizes, sandbox, @"for" }; +pub const TokenListKey = struct { element: *Element, attribute: TokenListAttribute }; +pub const TokenListLookup = std.AutoHashMapUnmanaged(TokenListKey, *collections.DOMTokenList); + +pub fn getTokenList(self: *Element, comptime attribute: TokenListAttribute, frame: *Frame) !*collections.DOMTokenList { + const gop = try frame._element_token_lists.getOrPut(frame.arena, .{ .element = self, .attribute = attribute }); + if (!gop.found_existing) { + gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{ + ._element = self, + ._attribute_name = comptime .wrap(@tagName(attribute)), + }); + } + return gop.value_ptr.*; +} + pub fn getDataset(self: *Element, frame: *Frame) !*DOMStringMap { const gop = try frame._element_datasets.getOrPut(frame.arena, self); if (!gop.found_existing) { @@ -1488,6 +1505,14 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { const tag_name = self.getTagNameDump(); const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, &self._attributes); + // A namespace outside the built-in set lives in a side table; the clone + // must report the same namespaceURI. + if (self._namespace == .unknown) { + if (frame._element_namespace_uris.get(self)) |uri| { + try frame._element_namespace_uris.put(frame.arena, node.as(Element), uri); + } + } + // Allow element-specific types to copy their runtime state _ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| { log.err(.dom, "element.clone.failed", .{ .err = err }); @@ -1899,7 +1924,9 @@ pub const JsApi = struct { fn _tagName(self: *Element, frame: *Frame) []const u8 { return self.getTagNameSpec(&frame.buf); } - pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{}); + // the frame-aware variant returns the original URI for namespaces + // outside the built-in set instead of the placeholder + pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{}); pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true }); fn _innerText(self: *Element, frame: *Frame) ![]const u8 { diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index f0c25e1c3..b083969be 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -138,7 +138,7 @@ pub const EventListenerCallback = union(enum) { function: js.Function, object: js.Object, }; -pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, exec: *js.Execution) !void { +pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: js.Nullable(EventListenerCallback), opts_: ?AddEventListenerOptions, exec: *js.Execution) !void { // Convert the options before the null-callback early return: per spec, // the dictionary conversion throws even when the callback is null. const options = blk: { @@ -160,7 +160,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi }; }; - const callback = callback_ orelse return; + const callback = callback_.value orelse return; const em_callback: EventManager.Callback = switch (callback) { .object => |obj| .{ .object = obj }, @@ -180,8 +180,8 @@ const RemoveEventListenerOptions = union(enum) { capture: bool = false, }; }; -pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, exec: *js.Execution) !void { - const callback = callback_ orelse return; +pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: js.Nullable(EventListenerCallback), opts_: ?RemoveEventListenerOptions, exec: *js.Execution) !void { + const callback = callback_.value orelse return; // For object callbacks, check if handleEvent exists if (callback == .object) { diff --git a/src/browser/webapi/element/html/Area.zig b/src/browser/webapi/element/html/Area.zig index 0c9c821bc..02b01a708 100644 --- a/src/browser/webapi/element/html/Area.zig +++ b/src/browser/webapi/element/html/Area.zig @@ -16,10 +16,12 @@ const std = @import("std"); const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); - const URL = @import("../../../URL.zig"); + const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); +const DOMTokenList = @import("../../collections.zig").DOMTokenList; + const HtmlElement = @import("../Html.zig"); const Area = @This(); @@ -248,6 +250,15 @@ pub fn setProtocol(self: *Area, value: []const u8, frame: *Frame) !void { try setHref(self, new_href, frame); } +pub fn getRelList(self: *Area, frame: *Frame) !?*DOMTokenList { + const element = self.asElement(); + // relList is only valid for HTML elements + if (element._namespace != .html) { + return null; + } + return element.getRelList(frame); +} + fn getResolvedHref(self: *Area, frame: *Frame) !?[:0]const u8 { const href = self.asElement().getAttributeSafe(comptime .wrap("href")) orelse return null; if (href.len == 0) { @@ -288,18 +299,8 @@ pub const JsApi = struct { pub const download = bridge.accessor(Area.getDownload, Area.setDownload, .{ .ce_reactions = true }); pub const rel = bridge.accessor(Area.getRel, Area.setRel, .{ .ce_reactions = true }); pub const referrerPolicy = bridge.accessor(Area.getReferrerPolicy, Area.setReferrerPolicy, .{ .ce_reactions = true }); - pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true }); pub const toString = bridge.function(Area.getHref, .{}); - - fn _getRelList(self: *Area, frame: *Frame) !?*@import("../../collections.zig").DOMTokenList { - const element = self.asElement(); - // relList is only valid for HTML and SVG elements - const namespace = element._namespace; - if (namespace != .html and namespace != .svg) { - return null; - } - return element.getRelList(frame); - } + pub const relList = bridge.accessor(Area.getRelList, null, .{ .null_as_undefined = true }); }; const testing = @import("../../../../testing.zig"); diff --git a/src/browser/webapi/element/html/IFrame.zig b/src/browser/webapi/element/html/IFrame.zig index a5163c8e4..e2e139049 100644 --- a/src/browser/webapi/element/html/IFrame.zig +++ b/src/browser/webapi/element/html/IFrame.zig @@ -20,10 +20,13 @@ const std = @import("std"); const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); -const Window = @import("../../Window.zig"); -const Document = @import("../../Document.zig"); + const Node = @import("../../Node.zig"); +const Window = @import("../../Window.zig"); const Element = @import("../../Element.zig"); +const Document = @import("../../Document.zig"); +const DOMTokenList = @import("../../collections.zig").DOMTokenList; + const HtmlElement = @import("../Html.zig"); const IFrame = @This(); @@ -80,6 +83,14 @@ pub fn setName(self: *IFrame, value: []const u8, frame: *Frame) !void { try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), frame); } +pub fn getSandbox(self: *IFrame, frame: *Frame) !?*DOMTokenList { + const element = self.asElement(); + if (element._namespace != .html) { + return null; + } + return element.getTokenList(.sandbox, frame); +} + pub const JsApi = struct { pub const bridge = js.Bridge(IFrame); @@ -93,6 +104,7 @@ pub const JsApi = struct { pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{ .ce_reactions = true }); pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{}); pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{}); + pub const sandbox = bridge.accessor(IFrame.getSandbox, null, .{ .null_as_undefined = true }); }; pub const Build = struct { diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index 7eb2190f5..2c06d3d8b 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -22,6 +22,8 @@ const Frame = @import("../../../Frame.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); +const DOMTokenList = @import("../../collections.zig").DOMTokenList; + const HtmlElement = @import("../Html.zig"); const Link = @This(); @@ -187,6 +189,23 @@ pub fn setTarget(self: *Link, value: []const u8, frame: *Frame) !void { return self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), frame); } +pub fn getSizes(self: *Link, frame: *Frame) !?*DOMTokenList { + const element = self.asElement(); + if (element._namespace != .html) { + return null; + } + return element.getTokenList(.sizes, frame); +} + +pub fn getRelList(self: *Link, frame: *Frame) !?*DOMTokenList { + const element = self.asElement(); + // relList is only valid for HTML elements, not SVG or MathML + if (element._namespace != .html) { + return null; + } + return element.getRelList(frame); +} + pub fn linkAddedCallback(self: *Link, frame: *Frame) !void { // if we're planning on navigating to another frame, don't trigger load event. if (frame.isGoingAway()) { @@ -255,16 +274,8 @@ pub const JsApi = struct { pub const @"type" = bridge.accessor(Link.getType, Link.setType, .{ .ce_reactions = true }); pub const rev = bridge.accessor(Link.getRev, Link.setRev, .{ .ce_reactions = true }); pub const target = bridge.accessor(Link.getTarget, Link.setTarget, .{ .ce_reactions = true }); - pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true }); - - fn _getRelList(self: *Link, frame: *Frame) !?*@import("../../collections.zig").DOMTokenList { - const element = self.asElement(); - // relList is only valid for HTML elements, not SVG or MathML - if (element._namespace != .html) { - return null; - } - return element.getRelList(frame); - } + pub const relList = bridge.accessor(Link.getRelList, null, .{ .null_as_undefined = true }); + pub const sizes = bridge.accessor(Link.getSizes, null, .{ .null_as_undefined = true }); }; // Parser-created elements are void (no closing tag) so they never diff --git a/src/browser/webapi/element/html/Output.zig b/src/browser/webapi/element/html/Output.zig index 207e03a4b..449f84ec6 100644 --- a/src/browser/webapi/element/html/Output.zig +++ b/src/browser/webapi/element/html/Output.zig @@ -1,7 +1,28 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); + const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); +const DOMTokenList = @import("../../collections.zig").DOMTokenList; + const HtmlElement = @import("../Html.zig"); const Output = @This(); @@ -19,6 +40,14 @@ pub fn getLabels(self: *Output, frame: *Frame) !js.Array { return @import("Label.zig").getControlLabels(self.asElement(), frame); } +pub fn getHtmlFor(self: *Output, frame: *Frame) !?*DOMTokenList { + const element = self.asElement(); + if (element._namespace != .html) { + return null; + } + return element.getTokenList(.@"for", frame); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Output); @@ -29,4 +58,5 @@ pub const JsApi = struct { }; pub const labels = bridge.accessor(Output.getLabels, null, .{}); + pub const htmlFor = bridge.accessor(Output.getHtmlFor, null, .{ .null_as_undefined = true }); };