mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
Merge pull request #2949 from lightpanda-io/dom-pr-08-idl-and-namespaces
webapi: IDL argument shapes, DOMTokenList reflection, namespace validation
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// 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/>.
|
||||
|
||||
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, .{});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <area> 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 <area> 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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <link> 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 <link> 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 <link> elements are void (no closing tag) so they never
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user