//
// 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 Node = @import("Node.zig");
const Element = @import("Element.zig");
pub const Full = TreeWalker(.full);
pub const FullExcludeSelf = TreeWalker(.exclude_self);
pub const Children = TreeWalker(.children);
const Mode = enum {
full,
children,
exclude_self,
};
pub fn TreeWalker(comptime mode: Mode) type {
return struct {
_current: ?*Node = null,
_next: ?*Node,
_root: *Node,
const Self = @This();
const Opts = struct {};
pub fn init(root: *Node, opts: Opts) Self {
_ = opts;
return .{
._next = firstNext(root),
._root = root,
};
}
pub fn next(self: *Self) ?*Node {
const node = self._next orelse return null;
self._current = node;
if (comptime mode == .children) {
self._next = node.nextSibling();
return node;
}
if (node.firstChild()) |child| {
self._next = child;
} else {
var current: *Node = node;
while (current != self._root) {
if (current.nextSibling()) |sibling| {
self._next = sibling;
return node;
}
current = current._parent orelse break;
}
self._next = null;
}
return node;
}
pub fn skipChildren(self: *Self) void {
if (comptime mode == .children) return;
const current_node = self._current orelse return;
var current: *Node = current_node;
while (current != self._root) {
if (current.nextSibling()) |sibling| {
self._next = sibling;
return;
}
current = current._parent orelse break;
}
self._next = null;
}
pub fn reset(self: *Self) void {
self._current = null;
self._next = firstNext(self._root);
}
pub fn contains(self: *const Self, target: *const Node) bool {
const root = self._root;
if (comptime mode == .children) {
var it = root.childrenIterator();
while (it.next()) |child| {
if (child == target) {
return true;
}
}
return false;
}
var node = target;
if ((comptime mode == .exclude_self) and node == root) {
return false;
}
while (true) {
if (node == root) {
return true;
}
node = node._parent orelse return false;
}
}
pub fn clone(self: *const Self) Self {
const root = self._root;
return .{
._next = firstNext(root),
._root = root,
};
}
fn firstNext(root: *Node) ?*Node {
return switch (comptime mode) {
.full => root,
.exclude_self => root.firstChild(),
.children => root.firstChild(),
};
}
pub const Elements = struct {
tw: Self,
pub fn init(root: *Node, comptime opts: Opts) Elements {
return .{
.tw = Self.init(root, opts),
};
}
pub fn next(self: *Elements) ?*Element {
while (self.tw.next()) |node| {
if (node.is(Element)) |el| {
return el;
}
}
return null;
}
pub fn reset(self: *Elements) void {
self.tw.reset();
}
};
};
}
test "TreeWalker: skipChildren" {
const testing = @import("../../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const doc = page.window._document;
//
const div = try doc.createElement("div", null, page);
const span = try doc.createElement("span", null, page);
const b = try doc.createElement("b", null, page);
const p = try doc.createElement("p", null, page);
_ = try span.asNode().appendChild(b.asNode(), page);
_ = try div.asNode().appendChild(span.asNode(), page);
_ = try div.asNode().appendChild(p.asNode(), page);
var tw = Full.init(div.asNode(), .{});
// root (div)
try testing.expect(tw.next() == div.asNode());
// span
try testing.expect(tw.next() == span.asNode());
// skip children of span (should jump over to )
tw.skipChildren();
try testing.expect(tw.next() == p.asNode());
try testing.expect(tw.next() == null);
}
================================================
FILE: src/browser/webapi/URL.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../js/js.zig");
const U = @import("../URL.zig");
const Page = @import("../Page.zig");
const URLSearchParams = @import("net/URLSearchParams.zig");
const Blob = @import("Blob.zig");
const Allocator = std.mem.Allocator;
const URL = @This();
_raw: [:0]const u8,
_arena: ?Allocator = null,
_search_params: ?*URLSearchParams = null,
// convenience
pub const resolve = @import("../URL.zig").resolve;
pub const eqlDocument = @import("../URL.zig").eqlDocument;
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
const base = if (base_) |b| blk: {
// If URL is absolute, base is ignored (but we still use page.url internally)
if (url_is_absolute) {
break :blk page.url;
}
// For relative URLs, base must be a valid absolute URL
if (!@import("../URL.zig").isCompleteHTTPUrl(b)) {
return error.TypeError;
}
break :blk b;
} else if (!url_is_absolute) {
return error.TypeError;
} else page.url;
const arena = page.arena;
const raw = try resolve(arena, base, url, .{ .always_dupe = true });
return page._factory.create(URL{
._raw = raw,
._arena = arena,
});
}
pub fn getUsername(self: *const URL) []const u8 {
return U.getUsername(self._raw);
}
pub fn setUsername(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setUsername(self._raw, value, allocator);
}
pub fn getPassword(self: *const URL) []const u8 {
return U.getPassword(self._raw);
}
pub fn setPassword(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setPassword(self._raw, value, allocator);
}
pub fn getPathname(self: *const URL) []const u8 {
return U.getPathname(self._raw);
}
pub fn getProtocol(self: *const URL) []const u8 {
return U.getProtocol(self._raw);
}
pub fn getHostname(self: *const URL) []const u8 {
return U.getHostname(self._raw);
}
pub fn getHost(self: *const URL) []const u8 {
return U.getHost(self._raw);
}
pub fn getPort(self: *const URL) []const u8 {
return U.getPort(self._raw);
}
pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 {
return (try U.getOrigin(page.call_arena, self._raw)) orelse {
// yes, a null string, that's what the spec wants
return "null";
};
}
pub fn getSearch(self: *const URL, page: *const Page) ![]const u8 {
// If searchParams has been accessed, generate search from it
if (self._search_params) |sp| {
if (sp.getSize() == 0) {
return "";
}
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try buf.writer.writeByte('?');
try sp.toString(&buf.writer);
return buf.written();
}
return U.getSearch(self._raw);
}
pub fn getHash(self: *const URL) []const u8 {
return U.getHash(self._raw);
}
pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams {
if (self._search_params) |sp| {
return sp;
}
// Get current search string (without the '?')
const search = try self.getSearch(page);
const search_value = if (search.len > 0) search[1..] else "";
const params = try URLSearchParams.init(.{ .query_string = search_value }, page);
self._search_params = params;
return params;
}
pub fn setHref(self: *URL, value: []const u8, page: *Page) !void {
const base = if (U.isCompleteHTTPUrl(value)) page.url else self._raw;
const raw = try U.resolve(self._arena orelse page.arena, base, value, .{ .always_dupe = true });
self._raw = raw;
// Update existing searchParams if it exists
if (self._search_params) |sp| {
const search = U.getSearch(raw);
const search_value = if (search.len > 0) search[1..] else "";
try sp.updateFromString(search_value, page);
}
}
pub fn setProtocol(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setProtocol(self._raw, value, allocator);
}
pub fn setHost(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setHost(self._raw, value, allocator);
}
pub fn setHostname(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setHostname(self._raw, value, allocator);
}
pub fn setPort(self: *URL, value: ?[]const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setPort(self._raw, value, allocator);
}
pub fn setPathname(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setPathname(self._raw, value, allocator);
}
pub fn setSearch(self: *URL, value: []const u8, page: *Page) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setSearch(self._raw, value, allocator);
// Update existing searchParams if it exists
if (self._search_params) |sp| {
const search = U.getSearch(self._raw);
const search_value = if (search.len > 0) search[1..] else "";
try sp.updateFromString(search_value, page);
}
}
pub fn setHash(self: *URL, value: []const u8) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setHash(self._raw, value, allocator);
}
pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {
const sp = self._search_params orelse {
return self._raw;
};
// Rebuild URL from searchParams
const raw = self._raw;
// Find the base (everything before ? or #)
const base_end = std.mem.indexOfAnyPos(u8, raw, 0, "?#") orelse raw.len;
const base = raw[0..base_end];
// Get the hash if it exists
const hash = self.getHash();
// Build the new URL string
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try buf.writer.writeAll(base);
// Add / if missing (e.g., "https://example.com" -> "https://example.com/")
// Only add if pathname is just "/" and not already in the base
const pathname = U.getPathname(raw);
if (std.mem.eql(u8, pathname, "/") and !std.mem.endsWith(u8, base, "/")) {
try buf.writer.writeByte('/');
}
// Only add ? if there are params
if (sp.getSize() > 0) {
try buf.writer.writeByte('?');
try sp.toString(&buf.writer);
}
try buf.writer.writeAll(hash);
try buf.writer.writeByte(0);
return buf.written()[0 .. buf.written().len - 1 :0];
}
pub fn canParse(url: []const u8, base_: ?[]const u8) bool {
if (base_) |b| {
return U.isCompleteHTTPUrl(b);
}
return U.isCompleteHTTPUrl(url);
}
pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
var uuid_buf: [36]u8 = undefined;
@import("../../id.zig").uuidv4(&uuid_buf);
const blob_url = try std.fmt.allocPrint(
page.arena,
"blob:{s}/{s}",
.{ page.origin orelse "null", uuid_buf },
);
try page._blob_urls.put(page.arena, blob_url, blob);
// prevent GC from cleaning up the blob while it's in the registry
page.js.strongRef(blob);
return blob_url;
}
pub fn revokeObjectURL(url: []const u8, page: *Page) void {
// Per spec: silently ignore non-blob URLs
if (!std.mem.startsWith(u8, url, "blob:")) {
return;
}
// Remove from registry and release strong ref (no-op if not found)
if (page._blob_urls.fetchRemove(url)) |entry| {
page.js.weakRef(entry.value);
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(URL);
pub const Meta = struct {
pub const name = "URL";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(URL.init, .{});
pub const canParse = bridge.function(URL.canParse, .{ .static = true });
pub const createObjectURL = bridge.function(URL.createObjectURL, .{ .static = true });
pub const revokeObjectURL = bridge.function(URL.revokeObjectURL, .{ .static = true });
pub const toString = bridge.function(URL.toString, .{});
pub const toJSON = bridge.function(URL.toString, .{});
pub const href = bridge.accessor(URL.toString, URL.setHref, .{});
pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});
pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});
pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});
pub const username = bridge.accessor(URL.getUsername, URL.setUsername, .{});
pub const password = bridge.accessor(URL.getPassword, URL.setPassword, .{});
pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});
pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});
pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});
pub const origin = bridge.accessor(URL.getOrigin, null, .{});
pub const protocol = bridge.accessor(URL.getProtocol, URL.setProtocol, .{});
pub const searchParams = bridge.accessor(URL.getSearchParams, null, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: URL" {
try testing.htmlRunner("url.html", .{});
}
================================================
FILE: src/browser/webapi/VisualViewport.zig
================================================
// 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 Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig");
const Window = @import("Window.zig");
const VisualViewport = @This();
_proto: *EventTarget,
pub fn asEventTarget(self: *VisualViewport) *EventTarget {
return self._proto;
}
pub fn getPageLeft(_: *const VisualViewport, page: *Page) u32 {
return page.window.getScrollX();
}
pub fn getPageTop(_: *const VisualViewport, page: *Page) u32 {
return page.window.getScrollY();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(VisualViewport);
pub const Meta = struct {
pub const name = "VisualViewport";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
// Static viewport properties for headless browser
// No pinch-zoom or mobile viewport, so values are straightforward
pub const offsetLeft = bridge.property(0, .{ .template = false });
pub const offsetTop = bridge.property(0, .{ .template = false });
pub const pageLeft = bridge.accessor(VisualViewport.getPageLeft, null, .{});
pub const pageTop = bridge.accessor(VisualViewport.getPageTop, null, .{});
pub const width = bridge.property(1920, .{ .template = false });
pub const height = bridge.property(1080, .{ .template = false });
pub const scale = bridge.property(1.0, .{ .template = false });
};
================================================
FILE: src/browser/webapi/Window.zig
================================================
// 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 std = @import("std");
const js = @import("../js/js.zig");
const builtin = @import("builtin");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const Console = @import("Console.zig");
const History = @import("History.zig");
const Navigation = @import("navigation/Navigation.zig");
const Crypto = @import("Crypto.zig");
const CSS = @import("CSS.zig");
const Navigator = @import("Navigator.zig");
const Screen = @import("Screen.zig");
const VisualViewport = @import("VisualViewport.zig");
const Performance = @import("Performance.zig");
const Document = @import("Document.zig");
const Location = @import("Location.zig");
const Fetch = @import("net/Fetch.zig");
const Event = @import("Event.zig");
const EventTarget = @import("EventTarget.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const MediaQueryList = @import("css/MediaQueryList.zig");
const storage = @import("storage/storage.zig");
const Element = @import("Element.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
const CustomElementRegistry = @import("CustomElementRegistry.zig");
const Selection = @import("Selection.zig");
const IS_DEBUG = builtin.mode == .Debug;
const Allocator = std.mem.Allocator;
const Window = @This();
_proto: *EventTarget,
_page: *Page,
_document: *Document,
_css: CSS = .init,
_crypto: Crypto = .init,
_console: Console = .init,
_navigator: Navigator = .init,
_screen: *Screen,
_visual_viewport: *VisualViewport,
_performance: Performance,
_storage_bucket: storage.Bucket = .{},
_on_load: ?js.Function.Global = null,
_on_pageshow: ?js.Function.Global = null,
_on_popstate: ?js.Function.Global = null,
_on_error: ?js.Function.Global = null,
_on_message: ?js.Function.Global = null,
_on_rejection_handled: ?js.Function.Global = null,
_on_unhandled_rejection: ?js.Function.Global = null,
_current_event: ?*Event = null,
_location: *Location,
_timer_id: u30 = 0,
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
_custom_elements: CustomElementRegistry = .{},
_scroll_pos: struct {
x: u32,
y: u32,
state: enum {
scroll,
end,
done,
},
} = .{
.x = 0,
.y = 0,
.state = .done,
},
pub fn asEventTarget(self: *Window) *EventTarget {
return self._proto;
}
pub fn getEvent(self: *const Window) ?*Event {
return self._current_event;
}
pub fn getSelf(self: *Window) *Window {
return self;
}
pub fn getWindow(self: *Window) *Window {
return self;
}
pub fn getTop(self: *Window) *Window {
var p = self._page;
while (p.parent) |parent| {
p = parent;
}
return p.window;
}
pub fn getParent(self: *Window) *Window {
if (self._page.parent) |p| {
return p.window;
}
return self;
}
pub fn getDocument(self: *Window) *Document {
return self._document;
}
pub fn getConsole(self: *Window) *Console {
return &self._console;
}
pub fn getNavigator(self: *Window) *Navigator {
return &self._navigator;
}
pub fn getScreen(self: *Window) *Screen {
return self._screen;
}
pub fn getVisualViewport(self: *const Window) *VisualViewport {
return self._visual_viewport;
}
pub fn getCrypto(self: *Window) *Crypto {
return &self._crypto;
}
pub fn getCSS(self: *Window) *CSS {
return &self._css;
}
pub fn getPerformance(self: *Window) *Performance {
return &self._performance;
}
pub fn getLocalStorage(self: *Window) *storage.Lookup {
return &self._storage_bucket.local;
}
pub fn getSessionStorage(self: *Window) *storage.Lookup {
return &self._storage_bucket.session;
}
pub fn getLocation(self: *const Window) *Location {
return self._location;
}
pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection;
}
pub fn setLocation(self: *Window, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._page });
}
pub fn getHistory(_: *Window, page: *Page) *History {
return &page._session.history;
}
pub fn getNavigation(_: *Window, page: *Page) *Navigation {
return &page._session.navigation;
}
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
return &self._custom_elements;
}
pub fn getOnLoad(self: *const Window) ?js.Function.Global {
return self._on_load;
}
pub fn setOnLoad(self: *Window, setter: ?FunctionSetter) void {
self._on_load = getFunctionFromSetter(setter);
}
pub fn getOnPageShow(self: *const Window) ?js.Function.Global {
return self._on_pageshow;
}
pub fn setOnPageShow(self: *Window, setter: ?FunctionSetter) void {
self._on_pageshow = getFunctionFromSetter(setter);
}
pub fn getOnPopState(self: *const Window) ?js.Function.Global {
return self._on_popstate;
}
pub fn setOnPopState(self: *Window, setter: ?FunctionSetter) void {
self._on_popstate = getFunctionFromSetter(setter);
}
pub fn getOnError(self: *const Window) ?js.Function.Global {
return self._on_error;
}
pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
self._on_error = getFunctionFromSetter(setter);
}
pub fn getOnMessage(self: *const Window) ?js.Function.Global {
return self._on_message;
}
pub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void {
self._on_message = getFunctionFromSetter(setter);
}
pub fn getOnRejectionHandled(self: *const Window) ?js.Function.Global {
return self._on_rejection_handled;
}
pub fn setOnRejectionHandled(self: *Window, setter: ?FunctionSetter) void {
self._on_rejection_handled = getFunctionFromSetter(setter);
}
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
return self._on_unhandled_rejection;
}
pub fn setOnUnhandledRejection(self: *Window, setter: ?FunctionSetter) void {
self._on_unhandled_rejection = getFunctionFromSetter(setter);
}
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise {
return Fetch.init(input, options, page);
}
pub fn setTimeout(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = false,
.params = params,
.low_priority = false,
.name = "window.setTimeout",
}, page);
}
pub fn setInterval(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = true,
.params = params,
.low_priority = false,
.name = "window.setInterval",
}, page);
}
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, 0, .{
.repeat = false,
.params = params,
.low_priority = false,
.name = "window.setImmediate",
}, page);
}
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, 5, .{
.repeat = false,
.params = &.{},
.low_priority = false,
.mode = .animation_frame,
.name = "window.requestAnimationFrame",
}, page);
}
pub fn queueMicrotask(_: *Window, cb: js.Function, page: *Page) void {
page.js.queueMicrotaskFunc(cb);
}
pub fn clearTimeout(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
sc.removed = true;
}
pub fn clearInterval(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
sc.removed = true;
}
pub fn clearImmediate(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
sc.removed = true;
}
pub fn cancelAnimationFrame(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
sc.removed = true;
}
const RequestIdleCallbackOpts = struct {
timeout: ?u32 = null,
};
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
const opts = opts_ orelse RequestIdleCallbackOpts{};
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
.mode = .idle,
.repeat = false,
.params = &.{},
.low_priority = true,
.name = "window.requestIdleCallback",
}, page);
}
pub fn cancelIdleCallback(self: *Window, id: u32) void {
var sc = self._timers.get(id) orelse return;
sc.removed = true;
}
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.temp(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
// If it returns true, the event is cancelled.
var prevent_default = false;
if (self._on_error) |on_error| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local_func = ls.toLocal(on_error);
const result = local_func.call(js.Value, .{
error_event._message,
error_event._filename,
error_event._line_number,
error_event._column_number,
err,
}) catch null;
// Per spec: returning true from onerror cancels the event
if (result) |r| {
prevent_default = r.isTrue();
}
}
const event = error_event.asEvent();
event._prevent_default = prevent_default;
// Pass null as handler: onerror was already called above with 5 args.
// We still dispatch so that addEventListener('error', ...) listeners fire.
try page._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{
.context = "window.reportError",
});
if (comptime builtin.is_test == false) {
if (!event._prevent_default) {
log.warn(.js, "window.reportError", .{
.message = error_event._message,
.filename = error_event._filename,
.line_number = error_event._line_number,
.column_number = error_event._column_number,
});
}
}
}
pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList {
return page._factory.eventTarget(MediaQueryList{
._proto = undefined,
._media = try page.dupeString(query),
});
}
pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]const u8, page: *Page) !*CSSStyleProperties {
if (pseudo_element) |pe| {
if (pe.len != 0) {
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pe });
}
}
return CSSStyleProperties.init(element, true, page);
}
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
// For now, we ignore targetOrigin checking and just dispatch the message
// In a full implementation, we would validate the origin
_ = target_origin;
// self = the window that will get the message
// page = the context calling postMessage
const target_page = self._page;
const source_window = target_page.js.getIncumbent().window;
const arena = try target_page.getArena(.{ .debug = "Window.postMessage" });
errdefer target_page.releaseArena(arena);
// Origin should be the source window's origin (where the message came from)
const origin = try source_window._location.getOrigin(page);
const callback = try arena.create(PostMessageCallback);
callback.* = .{
.arena = arena,
.message = message,
.page = target_page,
.source = source_window,
.origin = try arena.dupe(u8, origin),
};
try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "postMessage",
.low_priority = false,
.finalizer = PostMessageCallback.cancelled,
});
}
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
const encoded = try page.call_arena.alloc(u8, encoded_len);
return std.base64.standard.Encoder.encode(encoded, input);
}
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
// Forgiving base64 decode per WHATWG spec:
// https://infra.spec.whatwg.org/#forgiving-base64-decode
// Remove trailing padding to use standard_no_pad decoder
const unpadded = std.mem.trimRight(u8, trimmed, "=");
// Length % 4 == 1 is invalid (can't represent valid base64)
if (unpadded.len % 4 == 1) {
return error.InvalidCharacterError;
}
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
const decoded = try page.call_arena.alloc(u8, decoded_len);
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
return decoded;
}
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
return value.structuredClone();
}
pub fn getFrame(self: *Window, idx: usize) !?*Window {
const page = self._page;
const frames = page.frames.items;
if (idx >= frames.len) {
return null;
}
if (page.frames_sorted == false) {
std.mem.sort(*Page, frames, {}, struct {
fn lessThan(_: void, a: *Page, b: *Page) bool {
const iframe_a = a.iframe orelse return false;
const iframe_b = b.iframe orelse return true;
const pos = iframe_a.asNode().compareDocumentPosition(iframe_b.asNode());
// Return true if a precedes b (a should come before b in sorted order)
return (pos & 0x04) != 0; // FOLLOWING bit: b follows a
}
}.lessThan);
page.frames_sorted = true;
}
return frames[idx].window;
}
pub fn getFramesLength(self: *const Window) u32 {
return @intCast(self._page.frames.items.len);
}
pub fn getScrollX(self: *const Window) u32 {
return self._scroll_pos.x;
}
pub fn getScrollY(self: *const Window) u32 {
return self._scroll_pos.y;
}
const ScrollToOpts = union(enum) {
x: i32,
opts: Opts,
const Opts = struct {
top: i32,
left: i32,
behavior: []const u8 = "",
};
};
pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
switch (opts) {
.x => |x| {
self._scroll_pos.x = @intCast(@max(x, 0));
self._scroll_pos.y = @intCast(@max(0, y orelse 0));
},
.opts => |o| {
self._scroll_pos.x = @intCast(@max(0, o.left));
self._scroll_pos.y = @intCast(@max(0, o.top));
},
}
self._scroll_pos.state = .scroll;
// We dispatch scroll event asynchronously after 10ms. So we can throttle
// them.
try page.js.scheduler.add(
page,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
const pos = &p.window._scroll_pos;
// If the state isn't scroll, we can ignore safely to throttle
// the events.
if (pos.state != .scroll) {
return null;
}
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .end;
return null;
}
}.dispatch,
10,
.{ .low_priority = true },
);
// We dispatch scrollend event asynchronously after 20ms.
try page.js.scheduler.add(
page,
struct {
fn dispatch(_page: *anyopaque) anyerror!?u32 {
const p: *Page = @ptrCast(@alignCast(_page));
const pos = &p.window._scroll_pos;
// Dispatch only if the state is .end.
// If a scroll is pending, retry in 10ms.
// If the state is .end, the event has been dispatched, so
// ignore safely.
switch (pos.state) {
.scroll => return 10,
.end => {},
.done => return null,
}
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .done;
return null;
}
}.dispatch,
20,
.{ .low_priority = true },
);
}
pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
// The scroll is relative to the current position. So compute to new
// absolute position.
var absx: i32 = undefined;
var absy: i32 = undefined;
switch (opts) {
.x => |x| {
absx = @as(i32, @intCast(self._scroll_pos.x)) + x;
absy = @as(i32, @intCast(self._scroll_pos.y)) + (y orelse 0);
},
.opts => |o| {
absx = @as(i32, @intCast(self._scroll_pos.x)) + o.left;
absy = @as(i32, @intCast(self._scroll_pos.y)) + o.top;
},
}
return self.scrollTo(.{ .x = absx }, absy, page);
}
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
.value = rejection.reason(),
.stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse "???",
});
}
const event_name, const attribute_callback = blk: {
if (no_handler) {
break :blk .{ "unhandledrejection", self._on_unhandled_rejection };
}
break :blk .{ "rejectionhandled", self._on_rejection_handled };
};
const target = self.asEventTarget();
if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
.reason = if (rejection.reason()) |r| try r.temp() else null,
.promise = try rejection.promise().temp(),
}, page)).asEvent();
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
}
}
const ScheduleOpts = struct {
repeat: bool,
params: []js.Value.Temp,
name: []const u8,
low_priority: bool = false,
animation_frame: bool = false,
mode: ScheduleCallback.Mode = .normal,
};
fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
if (self._timers.count() > 512) {
// these are active
return error.TooManyTimeout;
}
const arena = try page.getArena(.{ .debug = "Window.schedule" });
errdefer page.releaseArena(arena);
const timer_id = self._timer_id +% 1;
self._timer_id = timer_id;
const params = opts.params;
var persisted_params: []js.Value.Temp = &.{};
if (params.len > 0) {
persisted_params = try arena.dupe(js.Value.Temp, params);
}
const gop = try self._timers.getOrPut(page.arena, timer_id);
if (gop.found_existing) {
// 2^31 would have to wrap for this to happen.
return error.TooManyTimeout;
}
errdefer _ = self._timers.remove(timer_id);
const callback = try arena.create(ScheduleCallback);
callback.* = .{
.cb = cb,
.page = page,
.arena = arena,
.mode = opts.mode,
.name = opts.name,
.timer_id = timer_id,
.params = persisted_params,
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
};
gop.value_ptr.* = callback;
try page.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
.name = opts.name,
.low_priority = opts.low_priority,
.finalizer = ScheduleCallback.cancelled,
});
return timer_id;
}
const ScheduleCallback = struct {
// for debugging
name: []const u8,
// window._timers key
timer_id: u31,
// delay, in ms, to repeat. When null, will be removed after the first time
repeat_ms: ?u32,
cb: js.Function.Temp,
mode: Mode,
page: *Page,
arena: Allocator,
removed: bool = false,
params: []const js.Value.Temp,
const Mode = enum {
idle,
normal,
animation_frame,
};
fn cancelled(ctx: *anyopaque) void {
var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
self.deinit();
}
fn deinit(self: *ScheduleCallback) void {
self.cb.release();
for (self.params) |param| {
param.release();
}
self.page.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
const page = self.page;
const window = page.window;
if (self.removed) {
_ = window._timers.remove(self.timer_id);
self.deinit();
return null;
}
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (self.mode) {
.idle => {
const IdleDeadline = @import("IdleDeadline.zig");
ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {
log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err });
};
},
.animation_frame => {
ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
};
},
.normal => {
ls.toLocal(self.cb).call(void, self.params) catch |err| {
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
};
},
}
ls.local.runMicrotasks();
if (self.repeat_ms) |ms| {
return ms;
}
defer self.deinit();
_ = window._timers.remove(self.timer_id);
return null;
}
};
const PostMessageCallback = struct {
page: *Page,
source: *Window,
arena: Allocator,
origin: []const u8,
message: js.Value.Temp,
fn deinit(self: *PostMessageCallback) void {
self.page.releaseArena(self.arena);
}
fn cancelled(ctx: *anyopaque) void {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
self.deinit();
}
fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const page = self.page;
const window = page.window;
const event_target = window.asEventTarget();
if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = self.message,
.origin = self.origin,
.source = self.source,
.bubbles = false,
.cancelable = false,
}, page)).asEvent();
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
}
return null;
}
};
const FunctionSetter = union(enum) {
func: js.Function.Global,
anything: js.Value,
};
// window.onload = {}; doesn't fail, but it doesn't do anything.
// seems like setting to null is ok (though, at least on Firefix, it preserves
// the original value, which we could do, but why?)
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
const setter = setter_ orelse return null;
return switch (setter) {
.func => |func| func, // Already a Global from bridge auto-conversion
.anything => null,
};
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Window);
pub const Meta = struct {
pub const name = "Window";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
pub const top = bridge.accessor(Window.getTop, null, .{});
pub const self = bridge.accessor(Window.getWindow, null, .{});
pub const window = bridge.accessor(Window.getWindow, null, .{});
pub const parent = bridge.accessor(Window.getParent, null, .{});
pub const navigator = bridge.accessor(Window.getNavigator, null, .{});
pub const screen = bridge.accessor(Window.getScreen, null, .{});
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
pub const history = bridge.accessor(Window.getHistory, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});
pub const CSS = bridge.accessor(Window.getCSS, null, .{});
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{});
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
pub const setTimeout = bridge.function(Window.setTimeout, .{});
pub const clearTimeout = bridge.function(Window.clearTimeout, .{});
pub const setInterval = bridge.function(Window.setInterval, .{});
pub const clearInterval = bridge.function(Window.clearInterval, .{});
pub const setImmediate = bridge.function(Window.setImmediate, .{});
pub const clearImmediate = bridge.function(Window.clearImmediate, .{});
pub const requestAnimationFrame = bridge.function(Window.requestAnimationFrame, .{});
pub const cancelAnimationFrame = bridge.function(Window.cancelAnimationFrame, .{});
pub const requestIdleCallback = bridge.function(Window.requestIdleCallback, .{});
pub const cancelIdleCallback = bridge.function(Window.cancelIdleCallback, .{});
pub const matchMedia = bridge.function(Window.matchMedia, .{});
pub const postMessage = bridge.function(Window.postMessage, .{});
pub const btoa = bridge.function(Window.btoa, .{});
pub const atob = bridge.function(Window.atob, .{ .dom_exception = true });
pub const reportError = bridge.function(Window.reportError, .{});
pub const structuredClone = bridge.function(Window.structuredClone, .{});
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
pub const getSelection = bridge.function(Window.getSelection, .{});
pub const frames = bridge.accessor(Window.getWindow, null, .{});
pub const index = bridge.indexed(Window.getFrame, null, .{ .null_as_undefined = true });
pub const length = bridge.accessor(Window.getFramesLength, null, .{});
pub const scrollX = bridge.accessor(Window.getScrollX, null, .{});
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{});
pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{});
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{});
pub const scrollTo = bridge.function(Window.scrollTo, .{});
pub const scroll = bridge.function(Window.scrollTo, .{});
pub const scrollBy = bridge.function(Window.scrollBy, .{});
// Return false since we don't have secure-context-only APIs implemented
// (webcam, geolocation, clipboard, etc.)
// This is safer and could help avoid processing errors by hinting at
// sites not to try to access those features
pub const isSecureContext = bridge.property(false, .{ .template = false });
pub const innerWidth = bridge.property(1920, .{ .template = false });
pub const innerHeight = bridge.property(1080, .{ .template = false });
pub const devicePixelRatio = bridge.property(1, .{ .template = false });
// This should return a window-like object in specific conditions. Would be
// pretty complicated to properly support I think.
pub const opener = bridge.property(null, .{ .template = false });
pub const alert = bridge.function(struct {
fn alert(_: *const Window, _: ?[]const u8) void {}
}.alert, .{ .noop = true });
pub const confirm = bridge.function(struct {
fn confirm(_: *const Window, _: ?[]const u8) bool {
return false;
}
}.confirm, .{});
pub const prompt = bridge.function(struct {
fn prompt(_: *const Window, _: ?[]const u8, _: ?[]const u8) ?[]const u8 {
return null;
}
}.prompt, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: Window" {
try testing.htmlRunner("window", .{});
}
test "WebApi: Window scroll" {
try testing.htmlRunner("window_scroll.html", .{});
}
test "WebApi: Window.onerror" {
try testing.htmlRunner("event/report_error.html", .{});
}
================================================
FILE: src/browser/webapi/XMLDocument.zig
================================================
// Copyright (C) 2023-2025 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 Document = @import("Document.zig");
const Node = @import("Node.zig");
const XMLDocument = @This();
_proto: *Document,
pub fn asDocument(self: *XMLDocument) *Document {
return self._proto;
}
pub fn asNode(self: *XMLDocument) *Node {
return self._proto.asNode();
}
pub fn asEventTarget(self: *XMLDocument) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(XMLDocument);
pub const Meta = struct {
pub const name = "XMLDocument";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
================================================
FILE: src/browser/webapi/XMLSerializer.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const dump = @import("../dump.zig");
const XMLSerializer = @This();
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
_pad: bool = false,
pub fn init() XMLSerializer {
return .{};
}
pub fn serializeToString(self: *const XMLSerializer, node: *Node, page: *Page) ![]const u8 {
_ = self;
var buf = std.Io.Writer.Allocating.init(page.call_arena);
if (node.is(Node.Document)) |doc| {
try dump.root(doc, .{ .shadow = .skip }, &buf.writer, page);
} else {
try dump.deep(node, .{ .shadow = .skip }, &buf.writer, page);
}
// Not sure about this trim. But `dump` is meant to display relatively
// pretty HTML, so it does include newlines, which can result in a trailing
// newline. XMLSerializer is a bit more strict.
return std.mem.trim(u8, buf.written(), &std.ascii.whitespace);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(XMLSerializer);
pub const Meta = struct {
pub const name = "XMLSerializer";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const constructor = bridge.constructor(XMLSerializer.init, .{});
pub const serializeToString = bridge.function(XMLSerializer.serializeToString, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: XMLSerializer" {
try testing.htmlRunner("xmlserializer.html", .{});
}
================================================
FILE: src/browser/webapi/animation/Animation.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Allocator = std.mem.Allocator;
const Animation = @This();
const PlayState = enum {
idle,
running,
paused,
finished,
};
_page: *Page,
_arena: Allocator,
_effect: ?js.Object.Global = null,
_timeline: ?js.Object.Global = null,
_ready_resolver: ?js.PromiseResolver.Global = null,
_finished_resolver: ?js.PromiseResolver.Global = null,
_startTime: ?f64 = null,
_onFinish: ?js.Function.Temp = null,
_playState: PlayState = .idle,
// Fake the animation by passing the states:
// .idle => .running once play() is called.
// .running => .finished after 10ms when update() is callback.
//
// TODO add support for effect and timeline
pub fn init(page: *Page) !*Animation {
const arena = try page.getArena(.{ .debug = "Animation" });
errdefer page.releaseArena(arena);
const self = try arena.create(Animation);
self.* = .{
._page = page,
._arena = arena,
};
return self;
}
pub fn deinit(self: *Animation, _: bool, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn play(self: *Animation, page: *Page) !void {
if (self._playState == .running) {
return;
}
// transition to running.
self._playState = .running;
// Schedule the transition from .running => .finished in 10ms.
page.js.strongRef(self);
try page.js.scheduler.add(
self,
Animation.update,
10,
.{ .name = "animation.update" },
);
}
pub fn pause(self: *Animation) void {
self._playState = .paused;
}
pub fn cancel(self: *Animation) void {
// Transition to idle. If the animation was .running, the already-scheduled
// update() callback will fire but see .idle state, skip the finish
// transition, and release the strong ref via weakRef() as normal.
self._playState = .idle;
}
pub fn finish(self: *Animation, page: *Page) void {
if (self._playState == .finished) {
return;
}
self._playState = .finished;
// resolve finished
if (self._finished_resolver) |resolver| {
page.js.local.?.toLocal(resolver).resolve("Animation.getFinished", self);
}
// call onfinish
if (self._onFinish) |func| {
page.js.local.?.toLocal(func).call(void, .{}) catch |err| {
log.warn(.js, "Animation._onFinish", .{ .err = err });
};
}
}
pub fn reverse(_: *Animation) void {
log.warn(.not_implemented, "Animation.reverse", .{});
}
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver();
self._finished_resolver = try resolver.persist();
return resolver.promise();
}
return page.js.toLocal(self._finished_resolver).?.promise();
}
// The ready promise is immediately resolved.
pub fn getReady(self: *Animation, page: *Page) !js.Promise {
if (self._ready_resolver == null) {
const resolver = page.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getReady", self);
self._ready_resolver = try resolver.persist();
return resolver.promise();
}
return page.js.toLocal(self._ready_resolver).?.promise();
}
pub fn getEffect(self: *const Animation) ?js.Object.Global {
return self._effect;
}
pub fn setEffect(self: *Animation, effect: ?js.Object.Global) !void {
self._effect = effect;
}
pub fn getTimeline(self: *const Animation) ?js.Object.Global {
return self._timeline;
}
pub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void {
self._timeline = timeline;
}
pub fn getStartTime(self: *const Animation) ?f64 {
return self._startTime;
}
pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void {
self._startTime = value;
// if the startTime is null, don't play the animation.
if (value == null) {
return;
}
return self.play(page);
}
pub fn getOnFinish(self: *const Animation) ?js.Function.Temp {
return self._onFinish;
}
// callback function transitionning from a state to another
fn update(ctx: *anyopaque) !?u32 {
const self: *Animation = @ptrCast(@alignCast(ctx));
switch (self._playState) {
.running => {
// transition to finished.
self._playState = .finished;
var ls: js.Local.Scope = undefined;
self._page.js.localScope(&ls);
defer ls.deinit();
// resolve finished
if (self._finished_resolver) |resolver| {
ls.toLocal(resolver).resolve("Animation.getFinished", self);
}
// call onfinish
if (self._onFinish) |func| {
ls.toLocal(func).call(void, .{}) catch |err| {
log.warn(.js, "Animation._onFinish", .{ .err = err });
};
}
},
.idle, .paused, .finished => {},
}
// No future change scheduled, set the object weak for garbage collection.
self._page.js.weakRef(self);
return null;
}
pub fn setOnFinish(self: *Animation, cb: ?js.Function.Temp) !void {
self._onFinish = cb;
}
pub fn playState(self: *const Animation) []const u8 {
return @tagName(self._playState);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Animation);
pub const Meta = struct {
pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Animation.deinit);
};
pub const play = bridge.function(Animation.play, .{});
pub const pause = bridge.function(Animation.pause, .{});
pub const cancel = bridge.function(Animation.cancel, .{});
pub const finish = bridge.function(Animation.finish, .{});
pub const reverse = bridge.function(Animation.reverse, .{});
pub const playState = bridge.accessor(Animation.playState, null, .{});
pub const pending = bridge.property(false, .{ .template = false });
pub const finished = bridge.accessor(Animation.getFinished, null, .{});
pub const ready = bridge.accessor(Animation.getReady, null, .{});
pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});
pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{});
pub const startTime = bridge.accessor(Animation.getStartTime, Animation.setStartTime, .{});
pub const onfinish = bridge.accessor(Animation.getOnFinish, Animation.setOnFinish, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: Animation" {
try testing.htmlRunner("animation/animation.html", .{});
}
================================================
FILE: src/browser/webapi/canvas/CanvasRenderingContext2D.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
const ImageData = @import("../ImageData.zig");
/// This class doesn't implement a `constructor`.
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
const CanvasRenderingContext2D = @This();
/// Fill color.
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
_fill_style: color.RGBA = color.RGBA.Named.black,
pub fn getFillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
try self._fill_style.format(&w.writer);
return w.written();
}
pub fn setFillStyle(
self: *CanvasRenderingContext2D,
value: []const u8,
) !void {
// Prefer the same fill_style if fails.
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
}
const WidthOrImageData = union(enum) {
width: u32,
image_data: *ImageData,
};
pub fn createImageData(
_: *const CanvasRenderingContext2D,
width_or_image_data: WidthOrImageData,
/// If `ImageData` variant preferred, this is null.
maybe_height: ?u32,
/// Can be used if width and height provided.
maybe_settings: ?ImageData.ConstructorSettings,
page: *Page,
) !*ImageData {
switch (width_or_image_data) {
.width => |width| {
const height = maybe_height orelse return error.TypeError;
return ImageData.init(width, height, maybe_settings, page);
},
.image_data => |image_data| {
return ImageData.init(image_data._width, image_data._height, null, page);
},
}
}
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
pub fn getImageData(
_: *const CanvasRenderingContext2D,
_: i32, // sx
_: i32, // sy
sw: i32,
sh: i32,
page: *Page,
) !*ImageData {
if (sw <= 0 or sh <= 0) {
return error.IndexSizeError;
}
return ImageData.init(@intCast(sw), @intCast(sh), null, page);
}
pub fn save(_: *CanvasRenderingContext2D) void {}
pub fn restore(_: *CanvasRenderingContext2D) void {}
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn rotate(_: *CanvasRenderingContext2D, _: f64) void {}
pub fn translate(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn transform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn setTransform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn resetTransform(_: *CanvasRenderingContext2D) void {}
pub fn setStrokeStyle(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn clearRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fillRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn strokeRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn beginPath(_: *CanvasRenderingContext2D) void {}
pub fn closePath(_: *CanvasRenderingContext2D) void {}
pub fn moveTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn lineTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn quadraticCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn bezierCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn arc(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}
pub fn arcTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn rect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fill(_: *CanvasRenderingContext2D) void {}
pub fn stroke(_: *CanvasRenderingContext2D) void {}
pub fn clip(_: *CanvasRenderingContext2D) void {}
pub fn fillText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub fn strokeText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(CanvasRenderingContext2D);
pub const Meta = struct {
pub const name = "CanvasRenderingContext2D";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const font = bridge.property("10px sans-serif", .{ .template = false, .readonly = false });
pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });
pub const globalCompositeOperation = bridge.property("source-over", .{ .template = false, .readonly = false });
pub const strokeStyle = bridge.property("#000000", .{ .template = false, .readonly = false });
pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });
pub const lineCap = bridge.property("butt", .{ .template = false, .readonly = false });
pub const lineJoin = bridge.property("miter", .{ .template = false, .readonly = false });
pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });
pub const textAlign = bridge.property("start", .{ .template = false, .readonly = false });
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true });
pub const rotate = bridge.function(CanvasRenderingContext2D.rotate, .{ .noop = true });
pub const translate = bridge.function(CanvasRenderingContext2D.translate, .{ .noop = true });
pub const transform = bridge.function(CanvasRenderingContext2D.transform, .{ .noop = true });
pub const setTransform = bridge.function(CanvasRenderingContext2D.setTransform, .{ .noop = true });
pub const resetTransform = bridge.function(CanvasRenderingContext2D.resetTransform, .{ .noop = true });
pub const clearRect = bridge.function(CanvasRenderingContext2D.clearRect, .{ .noop = true });
pub const fillRect = bridge.function(CanvasRenderingContext2D.fillRect, .{ .noop = true });
pub const strokeRect = bridge.function(CanvasRenderingContext2D.strokeRect, .{ .noop = true });
pub const beginPath = bridge.function(CanvasRenderingContext2D.beginPath, .{ .noop = true });
pub const closePath = bridge.function(CanvasRenderingContext2D.closePath, .{ .noop = true });
pub const moveTo = bridge.function(CanvasRenderingContext2D.moveTo, .{ .noop = true });
pub const lineTo = bridge.function(CanvasRenderingContext2D.lineTo, .{ .noop = true });
pub const quadraticCurveTo = bridge.function(CanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });
pub const bezierCurveTo = bridge.function(CanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });
pub const arc = bridge.function(CanvasRenderingContext2D.arc, .{ .noop = true });
pub const arcTo = bridge.function(CanvasRenderingContext2D.arcTo, .{ .noop = true });
pub const rect = bridge.function(CanvasRenderingContext2D.rect, .{ .noop = true });
pub const fill = bridge.function(CanvasRenderingContext2D.fill, .{ .noop = true });
pub const stroke = bridge.function(CanvasRenderingContext2D.stroke, .{ .noop = true });
pub const clip = bridge.function(CanvasRenderingContext2D.clip, .{ .noop = true });
pub const fillText = bridge.function(CanvasRenderingContext2D.fillText, .{ .noop = true });
pub const strokeText = bridge.function(CanvasRenderingContext2D.strokeText, .{ .noop = true });
};
const testing = @import("../../../testing.zig");
test "WebApi: CanvasRenderingContext2D" {
try testing.htmlRunner("canvas/canvas_rendering_context_2d.html", .{});
}
================================================
FILE: src/browser/webapi/canvas/OffscreenCanvas.zig
================================================
// 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Blob = @import("../Blob.zig");
const OffscreenCanvasRenderingContext2D = @import("OffscreenCanvasRenderingContext2D.zig");
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
const OffscreenCanvas = @This();
pub const _prototype_root = true;
_width: u32,
_height: u32,
/// Since there's no base class rendering contextes inherit from,
/// we're using tagged union.
const DrawingContext = union(enum) {
@"2d": *OffscreenCanvasRenderingContext2D,
};
pub fn constructor(width: u32, height: u32, page: *Page) !*OffscreenCanvas {
return page._factory.create(OffscreenCanvas{
._width = width,
._height = height,
});
}
pub fn getWidth(self: *const OffscreenCanvas) u32 {
return self._width;
}
pub fn setWidth(self: *OffscreenCanvas, value: u32) void {
self._width = value;
}
pub fn getHeight(self: *const OffscreenCanvas) u32 {
return self._height;
}
pub fn setHeight(self: *OffscreenCanvas, value: u32) void {
self._height = value;
}
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(OffscreenCanvasRenderingContext2D{});
return .{ .@"2d" = ctx };
}
return null;
}
/// Returns a Promise that resolves to a Blob containing the image.
/// Since we have no actual rendering, this returns an empty blob.
pub fn convertToBlob(_: *OffscreenCanvas, page: *Page) !js.Promise {
const blob = try Blob.init(null, null, page);
return page.js.local.?.resolvePromise(blob);
}
/// Returns an ImageBitmap with the rendered content (stub).
pub fn transferToImageBitmap(_: *OffscreenCanvas) ?void {
// ImageBitmap not implemented yet, return null
return null;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(OffscreenCanvas);
pub const Meta = struct {
pub const name = "OffscreenCanvas";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(OffscreenCanvas.constructor, .{});
pub const width = bridge.accessor(OffscreenCanvas.getWidth, OffscreenCanvas.setWidth, .{});
pub const height = bridge.accessor(OffscreenCanvas.getHeight, OffscreenCanvas.setHeight, .{});
pub const getContext = bridge.function(OffscreenCanvas.getContext, .{});
pub const convertToBlob = bridge.function(OffscreenCanvas.convertToBlob, .{});
pub const transferToImageBitmap = bridge.function(OffscreenCanvas.transferToImageBitmap, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: OffscreenCanvas" {
try testing.htmlRunner("canvas/offscreen_canvas.html", .{});
}
================================================
FILE: src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
const ImageData = @import("../ImageData.zig");
/// This class doesn't implement a `constructor`.
/// It can be obtained with a call to `OffscreenCanvas#getContext`.
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D
const OffscreenCanvasRenderingContext2D = @This();
/// Fill color.
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
_fill_style: color.RGBA = color.RGBA.Named.black,
pub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
try self._fill_style.format(&w.writer);
return w.written();
}
pub fn setFillStyle(
self: *OffscreenCanvasRenderingContext2D,
value: []const u8,
) !void {
// Prefer the same fill_style if fails.
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
}
const WidthOrImageData = union(enum) {
width: u32,
image_data: *ImageData,
};
pub fn createImageData(
_: *const OffscreenCanvasRenderingContext2D,
width_or_image_data: WidthOrImageData,
/// If `ImageData` variant preferred, this is null.
maybe_height: ?u32,
/// Can be used if width and height provided.
maybe_settings: ?ImageData.ConstructorSettings,
page: *Page,
) !*ImageData {
switch (width_or_image_data) {
.width => |width| {
const height = maybe_height orelse return error.TypeError;
return ImageData.init(width, height, maybe_settings, page);
},
.image_data => |image_data| {
return ImageData.init(image_data._width, image_data._height, null, page);
},
}
}
pub fn putImageData(_: *const OffscreenCanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
pub fn getImageData(
_: *const OffscreenCanvasRenderingContext2D,
_: i32, // sx
_: i32, // sy
sw: i32,
sh: i32,
page: *Page,
) !*ImageData {
if (sw <= 0 or sh <= 0) {
return error.IndexSizeError;
}
return ImageData.init(@intCast(sw), @intCast(sh), null, page);
}
pub fn save(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn rotate(_: *OffscreenCanvasRenderingContext2D, _: f64) void {}
pub fn translate(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn transform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn setTransform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn resetTransform(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn setStrokeStyle(_: *OffscreenCanvasRenderingContext2D, _: []const u8) void {}
pub fn clearRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fillRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn strokeRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn beginPath(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn closePath(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn moveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn lineTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn quadraticCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn bezierCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn arc(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}
pub fn arcTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn rect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fill(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn stroke(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn clip(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn fillText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub fn strokeText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(OffscreenCanvasRenderingContext2D);
pub const Meta = struct {
pub const name = "OffscreenCanvasRenderingContext2D";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const font = bridge.property("10px sans-serif", .{ .template = false, .readonly = false });
pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });
pub const globalCompositeOperation = bridge.property("source-over", .{ .template = false, .readonly = false });
pub const strokeStyle = bridge.property("#000000", .{ .template = false, .readonly = false });
pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });
pub const lineCap = bridge.property("butt", .{ .template = false, .readonly = false });
pub const lineJoin = bridge.property("miter", .{ .template = false, .readonly = false });
pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });
pub const textAlign = bridge.property("start", .{ .template = false, .readonly = false });
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
pub const fillStyle = bridge.accessor(OffscreenCanvasRenderingContext2D.getFillStyle, OffscreenCanvasRenderingContext2D.setFillStyle, .{});
pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true });
pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true });
pub const rotate = bridge.function(OffscreenCanvasRenderingContext2D.rotate, .{ .noop = true });
pub const translate = bridge.function(OffscreenCanvasRenderingContext2D.translate, .{ .noop = true });
pub const transform = bridge.function(OffscreenCanvasRenderingContext2D.transform, .{ .noop = true });
pub const setTransform = bridge.function(OffscreenCanvasRenderingContext2D.setTransform, .{ .noop = true });
pub const resetTransform = bridge.function(OffscreenCanvasRenderingContext2D.resetTransform, .{ .noop = true });
pub const clearRect = bridge.function(OffscreenCanvasRenderingContext2D.clearRect, .{ .noop = true });
pub const fillRect = bridge.function(OffscreenCanvasRenderingContext2D.fillRect, .{ .noop = true });
pub const strokeRect = bridge.function(OffscreenCanvasRenderingContext2D.strokeRect, .{ .noop = true });
pub const beginPath = bridge.function(OffscreenCanvasRenderingContext2D.beginPath, .{ .noop = true });
pub const closePath = bridge.function(OffscreenCanvasRenderingContext2D.closePath, .{ .noop = true });
pub const moveTo = bridge.function(OffscreenCanvasRenderingContext2D.moveTo, .{ .noop = true });
pub const lineTo = bridge.function(OffscreenCanvasRenderingContext2D.lineTo, .{ .noop = true });
pub const quadraticCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });
pub const bezierCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });
pub const arc = bridge.function(OffscreenCanvasRenderingContext2D.arc, .{ .noop = true });
pub const arcTo = bridge.function(OffscreenCanvasRenderingContext2D.arcTo, .{ .noop = true });
pub const rect = bridge.function(OffscreenCanvasRenderingContext2D.rect, .{ .noop = true });
pub const fill = bridge.function(OffscreenCanvasRenderingContext2D.fill, .{ .noop = true });
pub const stroke = bridge.function(OffscreenCanvasRenderingContext2D.stroke, .{ .noop = true });
pub const clip = bridge.function(OffscreenCanvasRenderingContext2D.clip, .{ .noop = true });
pub const fillText = bridge.function(OffscreenCanvasRenderingContext2D.fillText, .{ .noop = true });
pub const strokeText = bridge.function(OffscreenCanvasRenderingContext2D.strokeText, .{ .noop = true });
};
================================================
FILE: src/browser/webapi/canvas/WebGLRenderingContext.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
pub fn registerTypes() []const type {
return &.{
WebGLRenderingContext,
// Extension types should be runtime generated. We might want
// to revisit this.
Extension.Type.WEBGL_debug_renderer_info,
Extension.Type.WEBGL_lose_context,
};
}
const WebGLRenderingContext = @This();
/// On Chrome and Safari, a call to `getSupportedExtensions` returns total of 39.
/// The reference for it lists lesser number of extensions:
/// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions#extension_list
pub const Extension = union(enum) {
ANGLE_instanced_arrays: void,
EXT_blend_minmax: void,
EXT_clip_control: void,
EXT_color_buffer_half_float: void,
EXT_depth_clamp: void,
EXT_disjoint_timer_query: void,
EXT_float_blend: void,
EXT_frag_depth: void,
EXT_polygon_offset_clamp: void,
EXT_shader_texture_lod: void,
EXT_texture_compression_bptc: void,
EXT_texture_compression_rgtc: void,
EXT_texture_filter_anisotropic: void,
EXT_texture_mirror_clamp_to_edge: void,
EXT_sRGB: void,
KHR_parallel_shader_compile: void,
OES_element_index_uint: void,
OES_fbo_render_mipmap: void,
OES_standard_derivatives: void,
OES_texture_float: void,
OES_texture_float_linear: void,
OES_texture_half_float: void,
OES_texture_half_float_linear: void,
OES_vertex_array_object: void,
WEBGL_blend_func_extended: void,
WEBGL_color_buffer_float: void,
WEBGL_compressed_texture_astc: void,
WEBGL_compressed_texture_etc: void,
WEBGL_compressed_texture_etc1: void,
WEBGL_compressed_texture_pvrtc: void,
WEBGL_compressed_texture_s3tc: void,
WEBGL_compressed_texture_s3tc_srgb: void,
WEBGL_debug_renderer_info: *Type.WEBGL_debug_renderer_info,
WEBGL_debug_shaders: void,
WEBGL_depth_texture: void,
WEBGL_draw_buffers: void,
WEBGL_lose_context: *Type.WEBGL_lose_context,
WEBGL_multi_draw: void,
WEBGL_polygon_mode: void,
/// Reified enum type from the fields of this union.
const Kind = blk: {
const info = @typeInfo(Extension).@"union";
const fields = info.fields;
var items: [fields.len]std.builtin.Type.EnumField = undefined;
for (fields, 0..) |field, i| {
items[i] = .{ .name = field.name, .value = i };
}
break :blk @Type(.{
.@"enum" = .{
.tag_type = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1),
.fields = &items,
.decls = &.{},
.is_exhaustive = true,
},
});
};
/// Returns the `Extension.Kind` by its name.
fn find(name: []const u8) ?Kind {
// Just to make you really sad, this function has to be case-insensitive.
// So here we copy what's being done in `std.meta.stringToEnum` but replace
// the comparison function.
const kvs = comptime build_kvs: {
const T = Extension.Kind;
const EnumKV = struct { []const u8, T };
var kvs_array: [@typeInfo(T).@"enum".fields.len]EnumKV = undefined;
for (@typeInfo(T).@"enum".fields, 0..) |enumField, i| {
kvs_array[i] = .{ enumField.name, @field(T, enumField.name) };
}
break :build_kvs kvs_array[0..];
};
const Map = std.StaticStringMapWithEql(Extension.Kind, std.static_string_map.eqlAsciiIgnoreCase);
const map = Map.initComptime(kvs);
return map.get(name);
}
/// Extension types.
pub const Type = struct {
pub const WEBGL_debug_renderer_info = struct {
_: u8 = 0,
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
pub const JsApi = struct {
pub const bridge = js.Bridge(WEBGL_debug_renderer_info);
pub const Meta = struct {
pub const name = "WEBGL_debug_renderer_info";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const UNMASKED_VENDOR_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_VENDOR_WEBGL, .{ .template = false, .readonly = true });
pub const UNMASKED_RENDERER_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_RENDERER_WEBGL, .{ .template = false, .readonly = true });
};
};
pub const WEBGL_lose_context = struct {
_: u8 = 0,
pub fn loseContext(_: *const WEBGL_lose_context) void {}
pub fn restoreContext(_: *const WEBGL_lose_context) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(WEBGL_lose_context);
pub const Meta = struct {
pub const name = "WEBGL_lose_context";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const loseContext = bridge.function(WEBGL_lose_context.loseContext, .{ .noop = true });
pub const restoreContext = bridge.function(WEBGL_lose_context.restoreContext, .{ .noop = true });
};
};
};
};
/// This actually takes "GLenum" which, in fact, is a fancy way to say number.
/// Return value also depends on what's being passed as `pname`; we don't really
/// support any though.
pub fn getParameter(_: *const WebGLRenderingContext, pname: u32) []const u8 {
_ = pname;
return "";
}
/// Enables a WebGL extension.
pub fn getExtension(_: *const WebGLRenderingContext, name: []const u8, page: *Page) !?Extension {
const tag = Extension.find(name) orelse return null;
return switch (tag) {
.WEBGL_debug_renderer_info => {
const info = try page._factory.create(Extension.Type.WEBGL_debug_renderer_info{});
return .{ .WEBGL_debug_renderer_info = info };
},
.WEBGL_lose_context => {
const ctx = try page._factory.create(Extension.Type.WEBGL_lose_context{});
return .{ .WEBGL_lose_context = ctx };
},
inline else => |comptime_enum| @unionInit(Extension, @tagName(comptime_enum), {}),
};
}
/// Returns a list of all the supported WebGL extensions.
pub fn getSupportedExtensions(_: *const WebGLRenderingContext) []const []const u8 {
return std.meta.fieldNames(Extension.Kind);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(WebGLRenderingContext);
pub const Meta = struct {
pub const name = "WebGLRenderingContext";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const getParameter = bridge.function(WebGLRenderingContext.getParameter, .{});
pub const getExtension = bridge.function(WebGLRenderingContext.getExtension, .{});
pub const getSupportedExtensions = bridge.function(WebGLRenderingContext.getSupportedExtensions, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: WebGLRenderingContext" {
try testing.htmlRunner("canvas/webgl_rendering_context.html", .{});
}
================================================
FILE: src/browser/webapi/cdata/CDATASection.zig
================================================
// Copyright (C) 2023-2025 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 Text = @import("Text.zig");
const CDATASection = @This();
_proto: *Text,
pub const JsApi = struct {
pub const bridge = js.Bridge(CDATASection);
pub const Meta = struct {
pub const name = "CDATASection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
================================================
FILE: src/browser/webapi/cdata/Comment.zig
================================================
// Copyright (C) 2023-2025 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 Page = @import("../../Page.zig");
const CData = @import("../CData.zig");
const Comment = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, page: *Page) !*Comment {
const node = try page.createComment(if (str) |s| s.value else "");
return node.as(Comment);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Comment);
pub const Meta = struct {
pub const name = "Comment";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(Comment.init, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: CData.Text" {
try testing.htmlRunner("cdata/comment.html", .{});
}
================================================
FILE: src/browser/webapi/cdata/ProcessingInstruction.zig
================================================
// Copyright (C) 2023-2025 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 CData = @import("../CData.zig");
const ProcessingInstruction = @This();
_proto: *CData,
_target: []const u8,
pub fn getTarget(self: *const ProcessingInstruction) []const u8 {
return self._target;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(ProcessingInstruction);
pub const Meta = struct {
pub const name = "ProcessingInstruction";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const target = bridge.accessor(ProcessingInstruction.getTarget, null, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: ProcessingInstruction" {
try testing.htmlRunner("processing_instruction.html", .{});
}
================================================
FILE: src/browser/webapi/cdata/Text.zig
================================================
// Copyright (C) 2023-2025 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 String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CData = @import("../CData.zig");
const Text = @This();
_proto: *CData,
pub fn init(str: ?js.NullableString, page: *Page) !*Text {
const node = try page.createTextNode(if (str) |s| s.value else "");
return node.as(Text);
}
pub fn getWholeText(self: *Text) []const u8 {
return self._proto._data.str();
}
pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
const data = self._proto._data.str();
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
const new_data = data[byte_offset..];
const new_node = try page.createTextNode(new_data);
const new_text = new_node.as(Text);
const node = self._proto.asNode();
// Per DOM spec splitText: insert first (step 7a), then update ranges (7b-7e),
// then truncate original node (step 8).
if (node.parentNode()) |parent| {
const next_sibling = node.nextSibling();
_ = try parent.insertBefore(new_node, next_sibling, page);
// splitText-specific range updates (steps 7b-7e)
if (parent.getChildIndex(node)) |node_index| {
page.updateRangesForSplitText(node, new_node, @intCast(offset), parent, node_index);
}
}
// Step 8: truncate original node via replaceData(offset, count, "").
// Use replaceData instead of setData so live range updates fire
// (matters for detached text nodes where steps 7b-7e were skipped).
const length = self._proto.getLength();
try self._proto.replaceData(offset, length - offset, "", page);
return new_text;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Text);
pub const Meta = struct {
pub const name = "Text";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const constructor = bridge.constructor(Text.init, .{});
pub const wholeText = bridge.accessor(Text.getWholeText, null, .{});
pub const splitText = bridge.function(Text.splitText, .{ .dom_exception = true });
};
================================================
FILE: src/browser/webapi/children.zig
================================================
const std = @import("std");
const Node = @import("Node.zig");
const LinkedList = std.DoublyLinkedList;
// Our node._chilren is of type ?*NodeList. The extra (extra) indirection is to
// keep memory size down.
// First, a lot of nodes have no children. For these nodes, `?*NodeList = null`
// will take 8 bytes and require no allocations (because an optional pointer in
// Zig uses the address 0 to represent null, rather than a separate field).
// Second, a lot of nodes will have one child. For these nodes, we'll also only
// use 8 bytes, because @sizeOf(NodeList) == 8. This is the reason the
// list: *LinkedList is behind a pointer.
pub const Children = union(enum) {
one: *Node,
list: *LinkedList,
pub fn first(self: *const Children) *Node {
return switch (self.*) {
.one => |n| n,
.list => |list| Node.linkToNode(list.first.?),
};
}
pub fn last(self: *const Children) *Node {
return switch (self.*) {
.one => |n| n,
.list => |list| Node.linkToNode(list.last.?),
};
}
pub fn len(self: *const Children) u32 {
return switch (self.*) {
.one => 1,
.list => |list| @intCast(list.len()),
};
}
};
================================================
FILE: src/browser/webapi/collections/ChildNodes.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const Node = @import("../Node.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const GenericIterator = @import("iterator.zig").Entry;
// Optimized for node.childNodes, which has to be a live list.
// No need to go through a TreeWalker or add any filtering.
const ChildNodes = @This();
_arena: std.mem.Allocator,
_last_index: usize,
_last_length: ?u32,
_last_node: ?*std.DoublyLinkedList.Node,
_cached_version: usize,
_node: *Node,
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);
pub fn init(node: *Node, page: *Page) !*ChildNodes {
const arena = try page.getArena(.{ .debug = "ChildNodes" });
errdefer page.releaseArena(arena);
const self = try arena.create(ChildNodes);
self.* = .{
._node = node,
._arena = arena,
._last_index = 0,
._last_node = null,
._last_length = null,
._cached_version = page.version,
};
return self;
}
pub fn deinit(self: *const ChildNodes, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn length(self: *ChildNodes, page: *Page) !u32 {
if (self.versionCheck(page)) {
if (self._last_length) |cached_length| {
return cached_length;
}
}
const children = self._node._children orelse return 0;
// O(N)
const len = children.len();
self._last_length = len;
return len;
}
pub fn getAtIndex(self: *ChildNodes, index: usize, page: *Page) !?*Node {
_ = self.versionCheck(page);
var current = self._last_index;
var node: ?*std.DoublyLinkedList.Node = null;
if (index < current) {
current = 0;
node = self.first() orelse return null;
} else {
node = self._last_node orelse self.first() orelse return null;
}
defer self._last_index = current;
while (node) |n| {
if (index == current) {
self._last_node = n;
return Node.linkToNode(n);
}
current += 1;
node = n.next;
}
self._last_node = null;
return null;
}
pub fn first(self: *const ChildNodes) ?*std.DoublyLinkedList.Node {
return &(self._node._children orelse return null).first()._child_link;
}
pub fn keys(self: *ChildNodes, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
}
pub fn values(self: *ChildNodes, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
}
pub fn entries(self: *ChildNodes, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
}
fn versionCheck(self: *ChildNodes, page: *Page) bool {
const current = page.version;
if (current == self._cached_version) {
return true;
}
self._last_index = 0;
self._last_node = null;
self._last_length = null;
self._cached_version = current;
return false;
}
const NodeList = @import("NodeList.zig");
pub fn runtimeGenericWrap(self: *ChildNodes, page: *Page) !*NodeList {
return page._factory.create(NodeList{ ._data = .{ .child_nodes = self } });
}
const Iterator = struct {
index: u32 = 0,
list: *ChildNodes,
const Entry = struct { u32, *Node };
pub fn next(self: *Iterator, page: *Page) !?Entry {
const index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null;
self.index = index + 1;
return .{ index, node };
}
};
================================================
FILE: src/browser/webapi/collections/DOMTokenList.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const log = @import("../../../log.zig");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const GenericIterator = @import("iterator.zig").Entry;
pub const DOMTokenList = @This();
// There are a lot of inefficiencies in this code because the list is meant to
// be live, e.g. reflect changes to the underlying attribute. The only good news
// is that lists tend to be very short (often just 1 item).
_element: *Element,
_attribute_name: String,
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);
const Lookup = std.StringArrayHashMapUnmanaged(void);
const WHITESPACE = " \t\n\r\x0C";
pub fn length(self: *const DOMTokenList, page: *Page) !u32 {
const tokens = try self.getTokens(page);
return @intCast(tokens.count());
}
// TODO: soooo..inefficient
pub fn item(self: *const DOMTokenList, index: usize, page: *Page) !?[]const u8 {
var i: usize = 0;
const allocator = page.call_arena;
var seen: std.StringArrayHashMapUnmanaged(void) = .empty;
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
while (it.next()) |token| {
const gop = try seen.getOrPut(allocator, token);
if (!gop.found_existing) {
if (i == index) {
return token;
}
i += 1;
}
}
return null;
}
pub fn contains(self: *const DOMTokenList, search: []const u8) !bool {
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
while (it.next()) |token| {
if (std.mem.eql(u8, search, token)) {
return true;
}
}
return false;
}
pub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
for (tokens) |token| {
try validateToken(token);
}
var lookup = try self.getTokens(page);
const allocator = page.call_arena;
try lookup.ensureUnusedCapacity(allocator, tokens.len);
for (tokens) |token| {
try lookup.put(allocator, token, {});
}
try self.updateAttribute(lookup, page);
}
pub fn remove(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
for (tokens) |token| {
try validateToken(token);
}
var lookup = try self.getTokens(page);
for (tokens) |token| {
_ = lookup.orderedRemove(token);
}
try self.updateAttribute(lookup, page);
}
pub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, page: *Page) !bool {
try validateToken(token);
const has_token = try self.contains(token);
if (force) |f| {
if (f) {
if (!has_token) {
const tokens_to_add = [_][]const u8{token};
try self.add(&tokens_to_add, page);
}
return true;
} else {
if (has_token) {
const tokens_to_remove = [_][]const u8{token};
try self.remove(&tokens_to_remove, page);
}
return false;
}
} else {
if (has_token) {
const tokens_to_remove = [_][]const u8{token};
try self.remove(tokens_to_remove[0..], page);
return false;
} else {
const tokens_to_add = [_][]const u8{token};
try self.add(tokens_to_add[0..], page);
return true;
}
}
}
pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8, page: *Page) !bool {
// Validate in spec order: both empty first, then both whitespace
if (old_token.len == 0 or new_token.len == 0) {
return error.SyntaxError;
}
if (std.mem.indexOfAny(u8, old_token, WHITESPACE) != null) {
return error.InvalidCharacterError;
}
if (std.mem.indexOfAny(u8, new_token, WHITESPACE) != null) {
return error.InvalidCharacterError;
}
var lookup = try self.getTokens(page);
// Check if old_token exists
if (!lookup.contains(old_token)) {
return false;
}
// If replacing with the same token, still need to trigger mutation
if (std.mem.eql(u8, new_token, old_token)) {
try self.updateAttribute(lookup, page);
return true;
}
const allocator = page.call_arena;
// Build new token list preserving order but replacing old with new
var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count());
var replaced_old = false;
for (lookup.keys()) |token| {
if (std.mem.eql(u8, token, old_token) and !replaced_old) {
new_tokens.appendAssumeCapacity(new_token);
replaced_old = true;
} else if (std.mem.eql(u8, token, old_token)) {
// Subsequent occurrences of old_token: skip (remove duplicates)
continue;
} else if (std.mem.eql(u8, token, new_token) and replaced_old) {
// Occurrence of new_token AFTER replacement: skip (remove duplicate)
continue;
} else {
// Any other token (including new_token before replacement): keep it
new_tokens.appendAssumeCapacity(token);
}
}
// Rebuild lookup
var new_lookup: Lookup = .empty;
try new_lookup.ensureTotalCapacity(allocator, new_tokens.items.len);
for (new_tokens.items) |token| {
try new_lookup.put(allocator, token, {});
}
try self.updateAttribute(new_lookup, page);
return true;
}
pub fn getValue(self: *const DOMTokenList) []const u8 {
return self._element.getAttributeSafe(self._attribute_name) orelse "";
}
pub fn setValue(self: *DOMTokenList, value: String, page: *Page) !void {
try self._element.setAttribute(self._attribute_name, value, page);
}
pub fn keys(self: *DOMTokenList, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
}
pub fn values(self: *DOMTokenList, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
}
pub fn entries(self: *DOMTokenList, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
}
pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page: *Page) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
const allocator = page.call_arena;
var i: i32 = 0;
var seen: std.StringArrayHashMapUnmanaged(void) = .empty;
var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);
while (it.next()) |token| {
const gop = try seen.getOrPut(allocator, token);
if (gop.found_existing) {
continue;
}
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ token, i, self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
return;
};
i += 1;
}
}
fn getTokens(self: *const DOMTokenList, page: *Page) !Lookup {
const value = self.getValue();
if (value.len == 0) {
return .empty;
}
var list: Lookup = .empty;
const allocator = page.call_arena;
try list.ensureTotalCapacity(allocator, 4);
var it = std.mem.tokenizeAny(u8, value, WHITESPACE);
while (it.next()) |token| {
try list.put(allocator, token, {});
}
return list;
}
fn validateToken(token: []const u8) !void {
if (token.len == 0) {
return error.SyntaxError;
}
if (std.mem.indexOfAny(u8, token, &std.ascii.whitespace) != null) {
return error.InvalidCharacterError;
}
}
fn updateAttribute(self: *DOMTokenList, tokens: Lookup, page: *Page) !void {
if (tokens.count() > 0) {
const joined = try std.mem.join(page.call_arena, " ", tokens.keys());
return self._element.setAttribute(self._attribute_name, .wrap(joined), page);
}
// Only remove attribute if it didn't exist before (was null)
// If it existed (even as ""), set it to "" to preserve its existence
if (self._element.hasAttributeSafe(self._attribute_name)) {
try self._element.setAttribute(self._attribute_name, .wrap(""), page);
}
}
const Iterator = struct {
index: u32 = 0,
list: *DOMTokenList,
const Entry = struct { u32, []const u8 };
pub fn next(self: *Iterator, page: *Page) !?Entry {
const index = self.index;
const node = try self.list.item(index, page) orelse return null;
self.index = index + 1;
return .{ index, node };
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMTokenList);
pub const Meta = struct {
pub const name = "DOMTokenList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const length = bridge.accessor(DOMTokenList.length, null, .{});
pub const item = bridge.function(_item, .{});
fn _item(self: *const DOMTokenList, index: i32, page: *Page) !?[]const u8 {
if (index < 0) {
return null;
}
return self.item(@intCast(index), page);
}
pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true });
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true });
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true });
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true });
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{});
pub const toString = bridge.function(DOMTokenList.getValue, .{});
pub const keys = bridge.function(DOMTokenList.keys, .{});
pub const values = bridge.function(DOMTokenList.values, .{});
pub const entries = bridge.function(DOMTokenList.entries, .{});
pub const symbol_iterator = bridge.iterator(DOMTokenList.values, .{});
pub const forEach = bridge.function(DOMTokenList.forEach, .{});
pub const @"[]" = bridge.indexed(DOMTokenList.item, null, .{ .null_as_undefined = true });
};
================================================
FILE: src/browser/webapi/collections/HTMLAllCollection.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const HTMLAllCollection = @This();
_tw: TreeWalker.FullExcludeSelf,
_last_index: usize,
_last_length: ?u32,
_cached_version: usize,
pub fn init(root: *Node, page: *Page) HTMLAllCollection {
return .{
._last_index = 0,
._last_length = null,
._tw = TreeWalker.FullExcludeSelf.init(root, .{}),
._cached_version = page.version,
};
}
fn versionCheck(self: *HTMLAllCollection, page: *const Page) bool {
if (self._cached_version != page.version) {
self._cached_version = page.version;
self._last_index = 0;
self._last_length = null;
self._tw.reset();
return false;
}
return true;
}
pub fn length(self: *HTMLAllCollection, page: *const Page) u32 {
if (self.versionCheck(page)) {
if (self._last_length) |cached_length| {
return cached_length;
}
}
lp.assert(self._last_index == 0, "HTMLAllCollection.length", .{ .last_index = self._last_index });
var tw = &self._tw;
defer tw.reset();
var l: u32 = 0;
while (tw.next()) |node| {
if (node.is(Element) != null) {
l += 1;
}
}
self._last_length = l;
return l;
}
pub fn getAtIndex(self: *HTMLAllCollection, index: usize, page: *const Page) ?*Element {
_ = self.versionCheck(page);
var current = self._last_index;
if (index <= current) {
current = 0;
self._tw.reset();
}
defer self._last_index = current + 1;
const tw = &self._tw;
while (tw.next()) |node| {
if (node.is(Element)) |el| {
if (index == current) {
return el;
}
current += 1;
}
}
return null;
}
pub fn getByName(self: *HTMLAllCollection, name: []const u8, page: *Page) ?*Element {
// First, try fast ID lookup using the document's element map
if (page.document._elements_by_id.get(name)) |el| {
return el;
}
// Fall back to searching by name attribute
// Clone the tree walker to preserve _last_index optimization
_ = self.versionCheck(page);
var tw = self._tw.clone();
tw.reset();
while (tw.next()) |node| {
if (node.is(Element)) |el| {
if (el.getAttributeSafe(comptime .wrap("name"))) |attr_name| {
if (std.mem.eql(u8, attr_name, name)) {
return el;
}
}
}
}
return null;
}
const CAllAsFunctionArg = union(enum) {
index: u32,
id: []const u8,
};
pub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, page: *Page) ?*Element {
return switch (arg) {
.index => |i| self.getAtIndex(i, page),
.id => |id| self.getByName(id, page),
};
}
pub fn iterator(self: *HTMLAllCollection, page: *Page) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = self._tw.clone(),
}, page);
}
const GenericIterator = @import("iterator.zig").Entry;
pub const Iterator = GenericIterator(struct {
list: *HTMLAllCollection,
tw: TreeWalker.FullExcludeSelf,
pub fn next(self: *@This(), _: *Page) ?*Element {
while (self.tw.next()) |node| {
if (node.is(Element)) |el| {
return el;
}
}
return null;
}
}, null);
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLAllCollection);
pub const Meta = struct {
pub const name = "HTMLAllCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
// This is a very weird class that requires special JavaScript behavior
// this htmldda and callable are only used here..
pub const htmldda = true;
pub const callable = JsApi.callable;
};
pub const length = bridge.accessor(HTMLAllCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element {
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
}
pub const namedItem = bridge.function(HTMLAllCollection.getByName, .{});
pub const symbol_iterator = bridge.iterator(HTMLAllCollection.iterator, .{});
pub const callable = bridge.callable(HTMLAllCollection.callable, .{ .null_as_undefined = true });
};
================================================
FILE: src/browser/webapi/collections/HTMLCollection.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive;
const Mode = enum {
tag,
tag_name,
tag_name_ns,
class_name,
all_elements,
child_elements,
child_tag,
selected_options,
links,
anchors,
form,
empty,
};
const HTMLCollection = @This();
_data: union(Mode) {
tag: NodeLive(.tag),
tag_name: NodeLive(.tag_name),
tag_name_ns: NodeLive(.tag_name_ns),
class_name: NodeLive(.class_name),
all_elements: NodeLive(.all_elements),
child_elements: NodeLive(.child_elements),
child_tag: NodeLive(.child_tag),
selected_options: NodeLive(.selected_options),
links: NodeLive(.links),
anchors: NodeLive(.anchors),
form: NodeLive(.form),
empty: void,
},
pub fn length(self: *HTMLCollection, page: *const Page) u32 {
return switch (self._data) {
.empty => 0,
inline else => |*impl| impl.length(page),
};
}
pub fn getAtIndex(self: *HTMLCollection, index: usize, page: *const Page) ?*Element {
return switch (self._data) {
.empty => null,
inline else => |*impl| impl.getAtIndex(index, page),
};
}
pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element {
return switch (self._data) {
.empty => null,
inline else => |*impl| impl.getByName(name, page),
};
}
pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = switch (self._data) {
.tag => |*impl| .{ .tag = impl._tw.clone() },
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.tag_name_ns => |*impl| .{ .tag_name_ns = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() },
.all_elements => |*impl| .{ .all_elements = impl._tw.clone() },
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
.child_tag => |*impl| .{ .child_tag = impl._tw.clone() },
.selected_options => |*impl| .{ .selected_options = impl._tw.clone() },
.links => |*impl| .{ .links = impl._tw.clone() },
.anchors => |*impl| .{ .anchors = impl._tw.clone() },
.form => |*impl| .{ .form = impl._tw.clone() },
.empty => .empty,
},
}, page);
}
const GenericIterator = @import("iterator.zig").Entry;
pub const Iterator = GenericIterator(struct {
list: *HTMLCollection,
tw: union(Mode) {
tag: TreeWalker.FullExcludeSelf,
tag_name: TreeWalker.FullExcludeSelf,
tag_name_ns: TreeWalker.FullExcludeSelf,
class_name: TreeWalker.FullExcludeSelf,
all_elements: TreeWalker.FullExcludeSelf,
child_elements: TreeWalker.Children,
child_tag: TreeWalker.Children,
selected_options: TreeWalker.Children,
links: TreeWalker.FullExcludeSelf,
anchors: TreeWalker.FullExcludeSelf,
form: TreeWalker.FullExcludeSelf,
empty: void,
},
pub fn next(self: *@This(), _: *Page) ?*Element {
return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.tag_name_ns => |*impl| impl.nextTw(&self.tw.tag_name_ns),
.class_name => |*impl| impl.nextTw(&self.tw.class_name),
.all_elements => |*impl| impl.nextTw(&self.tw.all_elements),
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
.child_tag => |*impl| impl.nextTw(&self.tw.child_tag),
.selected_options => |*impl| impl.nextTw(&self.tw.selected_options),
.links => |*impl| impl.nextTw(&self.tw.links),
.anchors => |*impl| impl.nextTw(&self.tw.anchors),
.form => |*impl| impl.nextTw(&self.tw.form),
.empty => return null,
};
}
}, null);
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLCollection);
pub const Meta = struct {
pub const name = "HTMLCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const length = bridge.accessor(HTMLCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element {
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
}
pub const namedItem = bridge.function(HTMLCollection.getByName, .{});
pub const symbol_iterator = bridge.iterator(HTMLCollection.iterator, .{});
};
================================================
FILE: src/browser/webapi/collections/HTMLFormControlsCollection.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const NodeList = @import("NodeList.zig");
const RadioNodeList = @import("RadioNodeList.zig");
const HTMLCollection = @import("HTMLCollection.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const HTMLFormControlsCollection = @This();
_proto: *HTMLCollection,
pub const NamedItemResult = union(enum) {
element: *Element,
radio_node_list: *RadioNodeList,
};
pub fn length(self: *HTMLFormControlsCollection, page: *Page) u32 {
return self._proto.length(page);
}
pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, page: *Page) ?*Element {
return self._proto.getAtIndex(index, page);
}
pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Page) !?NamedItemResult {
if (name.len == 0) {
return null;
}
// We need special handling for radio, where multiple inputs can have the
// same name, but we also need to handle the [incorrect] case where non-
// radios share names.
var count: u32 = 0;
var first_element: ?*Element = null;
var it = try self.iterator();
while (it.next()) |element| {
const is_match = blk: {
if (element.getAttributeSafe(comptime .wrap("id"))) |id| {
if (std.mem.eql(u8, id, name)) {
break :blk true;
}
}
if (element.getAttributeSafe(comptime .wrap("name"))) |elem_name| {
if (std.mem.eql(u8, elem_name, name)) {
break :blk true;
}
}
break :blk false;
};
if (is_match) {
if (first_element == null) {
first_element = element;
}
count += 1;
if (count == 2) {
const radio_node_list = try page._factory.create(RadioNodeList{
._proto = undefined,
._form_collection = self,
._name = try page.dupeString(name),
});
radio_node_list._proto = try page._factory.create(NodeList{ ._data = .{ .radio_node_list = radio_node_list } });
return .{ .radio_node_list = radio_node_list };
}
}
}
if (count == 0) {
return null;
}
// case == 2 was handled inside the loop
if (comptime IS_DEBUG) {
std.debug.assert(count == 1);
}
return .{ .element = first_element.? };
}
// used internally, by HTMLFormControlsCollection and RadioNodeList
pub fn iterator(self: *HTMLFormControlsCollection) !Iterator {
const form_collection = self._proto._data.form;
return .{
.tw = form_collection._tw.clone(),
.nodes = form_collection,
};
}
// Used internally. Presents a nicer (more zig-like) iterator and strips away
// some of the abstraction.
pub const Iterator = struct {
tw: TreeWalker,
nodes: NodeLive,
const NodeLive = @import("node_live.zig").NodeLive(.form);
const TreeWalker = @import("../TreeWalker.zig").FullExcludeSelf;
pub fn next(self: *Iterator) ?*Element {
return self.nodes.nextTw(&self.tw);
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLFormControlsCollection);
pub const Meta = struct {
pub const name = "HTMLFormControlsCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const manage = false;
};
pub const length = bridge.accessor(HTMLFormControlsCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLFormControlsCollection.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLFormControlsCollection.namedItem, null, null, .{ .null_as_undefined = true });
pub const namedItem = bridge.function(HTMLFormControlsCollection.namedItem, .{});
};
================================================
FILE: src/browser/webapi/collections/HTMLOptionsCollection.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const HTMLCollection = @import("HTMLCollection.zig");
const HTMLOptionsCollection = @This();
_proto: *HTMLCollection,
_select: *@import("../element/html/Select.zig"),
// Forward length to HTMLCollection
pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 {
return self._proto.length(page);
}
// Forward indexed access to HTMLCollection
pub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, page: *Page) ?*Element {
return self._proto.getAtIndex(index, page);
}
pub fn getByName(self: *HTMLOptionsCollection, name: []const u8, page: *Page) ?*Element {
return self._proto.getByName(name, page);
}
// Forward selectedIndex to the owning select element
pub fn getSelectedIndex(self: *const HTMLOptionsCollection) i32 {
return self._select.getSelectedIndex();
}
pub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void {
return self._select.setSelectedIndex(index);
}
const Option = @import("../element/html/Option.zig");
const AddBeforeOption = union(enum) {
option: *Option,
index: u32,
};
// Add a new option element
pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, page: *Page) !void {
const select_node = self._select.asNode();
const element_node = element.asElement().asNode();
var before_node: ?*Node = null;
if (before_) |before| {
switch (before) {
.index => |idx| {
if (self.getAtIndex(idx, page)) |el| {
before_node = el.asNode();
}
},
.option => |before_option| before_node = before_option.asNode(),
}
}
_ = try select_node.insertBefore(element_node, before_node, page);
}
// Remove an option element by index
pub fn remove(self: *HTMLOptionsCollection, index: i32, page: *Page) void {
if (index < 0) {
return;
}
if (self._proto.getAtIndex(@intCast(index), page)) |element| {
element.remove(page);
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLOptionsCollection);
pub const Meta = struct {
pub const name = "HTMLOptionsCollection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const manage = false;
};
pub const length = bridge.accessor(HTMLOptionsCollection.length, null, .{});
// Indexed access
pub const @"[int]" = bridge.indexed(HTMLOptionsCollection.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{});
pub const add = bridge.function(HTMLOptionsCollection.add, .{});
pub const remove = bridge.function(HTMLOptionsCollection.remove, .{});
};
================================================
FILE: src/browser/webapi/collections/NodeList.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Node = @import("../Node.zig");
const ChildNodes = @import("ChildNodes.zig");
const RadioNodeList = @import("RadioNodeList.zig");
const SelectorList = @import("../selector/List.zig");
const NodeLive = @import("node_live.zig").NodeLive;
const NodeList = @This();
_data: union(enum) {
child_nodes: *ChildNodes,
selector_list: *SelectorList,
radio_node_list: *RadioNodeList,
name: NodeLive(.name),
},
_rc: usize = 0,
pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
const rc = self._rc;
if (rc > 1) {
self._rc = rc - 1;
return;
}
switch (self._data) {
.selector_list => |list| list.deinit(session),
.child_nodes => |cn| cn.deinit(session),
else => {},
}
}
pub fn acquireRef(self: *NodeList) void {
self._rc += 1;
}
pub fn length(self: *NodeList, page: *Page) !u32 {
return switch (self._data) {
.child_nodes => |impl| impl.length(page),
.selector_list => |impl| @intCast(impl.getLength()),
.radio_node_list => |impl| impl.getLength(),
.name => |*impl| impl.length(page),
};
}
pub fn indexedGet(self: *NodeList, index: usize, page: *Page) !*Node {
return try self.getAtIndex(index, page) orelse return error.NotHandled;
}
pub fn getAtIndex(self: *NodeList, index: usize, page: *Page) !?*Node {
return switch (self._data) {
.child_nodes => |impl| impl.getAtIndex(index, page),
.selector_list => |impl| impl.getAtIndex(index),
.radio_node_list => |impl| impl.getAtIndex(index, page),
.name => |*impl| if (impl.getAtIndex(index, page)) |el| el.asNode() else null,
};
}
pub fn keys(self: *NodeList, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
}
pub fn values(self: *NodeList, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
}
pub fn entries(self: *NodeList, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
}
pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
var i: i32 = 0;
var it = try self.values(page);
while (true) : (i += 1) {
const next = try it.next(page);
if (next.done) {
return;
}
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ next.value, i, self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "nodelist" });
return;
};
}
}
const GenericIterator = @import("iterator.zig").Entry;
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);
const Iterator = struct {
index: u32 = 0,
list: *NodeList,
const Entry = struct { u32, *Node };
pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void {
self.list.deinit(shutdown, session);
}
pub fn acquireRef(self: *Iterator) void {
self.list.acquireRef();
}
pub fn next(self: *Iterator, page: *Page) !?Entry {
const index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null;
self.index = index + 1;
return .{ index, node };
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(NodeList);
pub const Meta = struct {
pub const name = "NodeList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
pub const weak = true;
pub const finalizer = bridge.finalizer(NodeList.deinit);
};
pub const length = bridge.accessor(NodeList.length, null, .{});
pub const @"[]" = bridge.indexed(NodeList.indexedGet, getIndexes, .{ .null_as_undefined = true });
pub const item = bridge.function(NodeList.getAtIndex, .{});
pub const keys = bridge.function(NodeList.keys, .{});
pub const values = bridge.function(NodeList.values, .{});
pub const entries = bridge.function(NodeList.entries, .{});
pub const forEach = bridge.function(NodeList.forEach, .{});
pub const symbol_iterator = bridge.iterator(NodeList.values, .{});
fn getIndexes(self: *NodeList, page: *Page) !js.Array {
const len = try self.length(page);
var arr = page.js.local.?.newArray(len);
for (0..len) |i| {
_ = try arr.set(@intCast(i), i, .{});
}
return arr;
}
};
================================================
FILE: src/browser/webapi/collections/RadioNodeList.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const Input = @import("../element/html/Input.zig");
const NodeList = @import("NodeList.zig");
const HTMLFormControlsCollection = @import("HTMLFormControlsCollection.zig");
const RadioNodeList = @This();
_proto: *NodeList,
_name: []const u8,
_form_collection: *HTMLFormControlsCollection,
pub fn getLength(self: *RadioNodeList) !u32 {
var i: u32 = 0;
var it = try self._form_collection.iterator();
while (it.next()) |element| {
if (self.matches(element)) {
i += 1;
}
}
return i;
}
pub fn getAtIndex(self: *RadioNodeList, index: usize, page: *Page) !?*Node {
var i: usize = 0;
var current: usize = 0;
while (self._form_collection.getAtIndex(i, page)) |element| : (i += 1) {
if (!self.matches(element)) {
continue;
}
if (current == index) {
return element.asNode();
}
current += 1;
}
return null;
}
pub fn getValue(self: *RadioNodeList) ![]const u8 {
var it = try self._form_collection.iterator();
while (it.next()) |element| {
const input = element.is(Input) orelse continue;
if (input._input_type != .radio) {
continue;
}
if (!input.getChecked()) {
continue;
}
return element.getAttributeSafe(comptime .wrap("value")) orelse "on";
}
return "";
}
pub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {
var it = try self._form_collection.iterator();
while (it.next()) |element| {
const input = element.is(Input) orelse continue;
if (input._input_type != .radio) {
continue;
}
const input_value = element.getAttributeSafe(comptime .wrap("value"));
const matches_value = blk: {
if (std.mem.eql(u8, value, "on")) {
break :blk input_value == null or (input_value != null and std.mem.eql(u8, input_value.?, "on"));
} else {
break :blk input_value != null and std.mem.eql(u8, input_value.?, value);
}
};
if (matches_value) {
try input.setChecked(true, page);
return;
}
}
}
fn matches(self: *const RadioNodeList, element: *Element) bool {
if (element.getAttributeSafe(comptime .wrap("id"))) |id| {
if (std.mem.eql(u8, id, self._name)) {
return true;
}
}
if (element.getAttributeSafe(comptime .wrap("name"))) |elem_name| {
if (std.mem.eql(u8, elem_name, self._name)) {
return true;
}
}
return false;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(RadioNodeList);
pub const Meta = struct {
pub const name = "RadioNodeList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const length = bridge.accessor(RadioNodeList.getLength, null, .{});
pub const @"[]" = bridge.indexed(RadioNodeList.getAtIndex, null, .{ .null_as_undefined = true });
pub const item = bridge.function(RadioNodeList.getAtIndex, .{});
pub const value = bridge.accessor(RadioNodeList.getValue, RadioNodeList.setValue, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: RadioNodeList" {
try testing.htmlRunner("collections/radio_node_list.html", .{});
}
================================================
FILE: src/browser/webapi/collections/iterator.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
const R = reflect(Inner, field);
return struct {
inner: Inner,
const Self = @This();
const Result = struct {
done: bool,
value: ?R.ValueType,
pub const js_as_object = true;
};
pub fn init(inner: Inner, page: *Page) !*Self {
return page._factory.create(Self{ .inner = inner });
}
pub fn deinit(self: *Self, shutdown: bool, session: *Session) void {
if (@hasDecl(Inner, "deinit")) {
self.inner.deinit(shutdown, session);
}
}
pub fn acquireRef(self: *Self) void {
if (@hasDecl(Inner, "acquireRef")) {
self.inner.acquireRef();
}
}
pub fn next(self: *Self, page: *Page) if (R.has_error_return) anyerror!Result else Result {
const entry = (if (comptime R.has_error_return) try self.inner.next(page) else self.inner.next(page)) orelse {
return .{ .done = true, .value = null };
};
if (comptime field == null) {
return .{ .done = false, .value = entry };
}
return .{
.done = false,
.value = @field(entry, field.?),
};
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Self);
pub const Meta = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Self.deinit);
};
pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });
pub const symbol_iterator = bridge.iterator(Self, .{});
};
};
}
fn reflect(comptime Inner: type, comptime field: ?[]const u8) Reflect {
const R = @typeInfo(@TypeOf(Inner.next)).@"fn".return_type.?;
const has_error_return = @typeInfo(R) == .error_union;
return .{
.has_error_return = has_error_return,
.ValueType = ValueType(unwrapOptional(unwrapError(R)), field),
};
}
const Reflect = struct {
has_error_return: bool,
ValueType: type,
};
fn unwrapError(comptime T: type) type {
if (@typeInfo(T) == .error_union) {
return @typeInfo(T).error_union.payload;
}
return T;
}
fn unwrapOptional(comptime T: type) type {
return @typeInfo(T).optional.child;
}
fn ValueType(comptime R: type, comptime field_: ?[]const u8) type {
const field = field_ orelse return R;
inline for (@typeInfo(R).@"struct".fields) |f| {
if (comptime std.mem.eql(u8, f.name, field)) {
return f.type;
}
}
@compileError("Unknown EntryIterator field " ++ @typeName(R) ++ "." ++ field);
}
================================================
FILE: src/browser/webapi/collections/node_live.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const lp = @import("lightpanda");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const Selector = @import("../selector/Selector.zig");
const Form = @import("../element/html/Form.zig");
const Mode = enum {
tag,
tag_name,
tag_name_ns,
class_name,
name,
all_elements,
child_elements,
child_tag,
selected_options,
links,
anchors,
form,
};
pub const TagNameNsFilter = struct {
namespace: ?Element.Namespace, // null means wildcard "*"
local_name: String,
};
const Filters = union(Mode) {
tag: Element.Tag,
tag_name: String,
tag_name_ns: TagNameNsFilter,
class_name: [][]const u8,
name: []const u8,
all_elements,
child_elements,
child_tag: Element.Tag,
selected_options,
links,
anchors,
form: *Form,
fn TypeOf(comptime mode: Mode) type {
@setEvalBranchQuota(2000);
return std.meta.fieldInfo(Filters, mode).type;
}
};
// Operations on the live DOM can be inefficient. Do we really have to walk
// through the entire tree, filtering out elements we don't care about, every
// time .length is called?
// To improve this, we track the "version" of the DOM (root.version). If the
// version changes between operations, than we have to restart and pay the full
// price.
// But, if the version hasn't changed, then we can leverage other stateful data
// to improve performance. For example, we cache the length property. So once
// we've walked the tree to figure the length, we can re-use the cached property
// if the DOM is unchanged (i.e. if our _cached_version == page.version).
//
// We do something similar for indexed getter (e.g. coll[4]), by preserving the
// last node visited in the tree (implicitly by not resetting the TreeWalker).
// If the DOM version is unchanged and the new index >= the last one, we can do
// not have to reset our TreeWalker. This optimizes the common case of accessing
// the collection via incrementing indexes.
pub fn NodeLive(comptime mode: Mode) type {
const Filter = Filters.TypeOf(mode);
const TW = switch (mode) {
.tag, .tag_name, .tag_name_ns, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
.child_elements, .child_tag, .selected_options => TreeWalker.Children,
};
return struct {
_tw: TW,
_filter: Filter,
_last_index: usize,
_last_length: ?u32,
_cached_version: usize,
const Self = @This();
pub fn init(root: *Node, filter: Filter, page: *Page) Self {
return .{
._last_index = 0,
._last_length = null,
._filter = filter,
._tw = TW.init(root, .{}),
._cached_version = page.version,
};
}
pub fn length(self: *Self, page: *const Page) u32 {
if (self.versionCheck(page)) {
// the DOM version hasn't changed, use the cached version if
// we have one
if (self._last_length) |cached_length| {
return cached_length;
}
// not ideal, but this can happen if list[x] is called followed
// by list.length.
self._tw.reset();
self._last_index = 0;
}
// If we're here, it means it's either the first time we're called
// or the DOM version has changed. Either way, the _tw should be
// at the start position. It's important that self._last_index == 0
// (which it always should be in these cases), because we're going to
// reset _tw at the end of this, _last_index should always be 0 when
// _tw is reset. Again, this should always be the case, but we're
// asserting to make sure, else we'll have weird behavior, namely
// the wrong item being returned for the wrong index.
lp.assert(self._last_index == 0, "NodeLives.length", .{ .last_index = self._last_index });
var tw = &self._tw;
defer tw.reset();
var l: u32 = 0;
while (self.nextTw(tw)) |_| {
l += 1;
}
self._last_length = l;
return l;
}
// This API supports indexing by both numeric index and id/name
// i.e. a combination of getAtIndex and getByName
pub fn getIndexed(self: *Self, value: js.Atom, page: *Page) !?*Element {
if (value.isUint()) |n| {
return self.getAtIndex(n, page);
}
const name = value.toString();
defer value.freeString(name);
return self.getByName(name, page) orelse return error.NotHandled;
}
pub fn getAtIndex(self: *Self, index: usize, page: *const Page) ?*Element {
_ = self.versionCheck(page);
var current = self._last_index;
if (index <= current) {
current = 0;
self._tw.reset();
}
defer self._last_index = current + 1;
const tw = &self._tw;
while (self.nextTw(tw)) |el| {
if (index == current) {
return el;
}
current += 1;
}
return null;
}
pub fn getByName(self: *Self, name: []const u8, page: *Page) ?*Element {
if (page.document.getElementById(name, page)) |element| {
const node = element.asNode();
if (self._tw.contains(node) and self.matches(node)) {
return element;
}
}
// Element not found by id, fallback to search by name. This isn't
// efficient!
// Gives us a TreeWalker based on the original, but reset to the
// root. Doing this preserves any cache data we have for other calls
// (like length or getAtIndex)
var tw = self._tw.clone();
while (self.nextTw(&tw)) |element| {
const element_name = element.getAttributeSafe(comptime .wrap("name")) orelse continue;
if (std.mem.eql(u8, element_name, name)) {
return element;
}
}
return null;
}
pub fn next(self: *Self) ?*Element {
return self.nextTw(&self._tw);
}
pub fn nextTw(self: *Self, tw: *TW) ?*Element {
while (tw.next()) |node| {
if (self.matches(node)) {
return node.as(Element);
}
}
return null;
}
fn matches(self: *const Self, node: *Node) bool {
switch (mode) {
.tag => {
const el = node.is(Element) orelse return false;
// For HTML namespace elements, we can use the optimized tag comparison.
// For other namespaces (XML, SVG custom elements, etc.), fall back to string comparison.
if (el._namespace == .html) {
return el.getTag() == self._filter;
}
// For non-HTML elements, compare by tag name string
const element_tag = el.getTagNameLower();
return std.mem.eql(u8, element_tag, @tagName(self._filter));
},
.tag_name => {
// If we're in `tag_name` mode, then the tag_name isn't
// a known tag. It could be a custom element, heading, or
// any generic element. Compare against the element's tag name.
// Per spec, getElementsByTagName is case-insensitive for HTML
// namespace elements, case-sensitive for others.
const el = node.is(Element) orelse return false;
const element_tag = el.getTagNameLower();
if (el._namespace == .html) {
return std.ascii.eqlIgnoreCase(element_tag, self._filter.str());
}
return std.mem.eql(u8, element_tag, self._filter.str());
},
.tag_name_ns => {
const el = node.is(Element) orelse return false;
if (self._filter.namespace) |ns| {
if (el._namespace != ns) return false;
}
// ok, namespace matches, check local name
if (self._filter.local_name.eql(comptime .wrap("*"))) {
// wildcard, match-all
return true;
}
return self._filter.local_name.eqlSlice(el.getLocalName());
},
.class_name => {
if (self._filter.len == 0) {
return false;
}
const el = node.is(Element) orelse return false;
const class_attr = el.getAttributeSafe(comptime .wrap("class")) orelse return false;
for (self._filter) |class_name| {
if (!Selector.classAttributeContains(class_attr, class_name)) {
return false;
}
}
return true;
},
.name => {
const el = node.is(Element) orelse return false;
const name_attr = el.getAttributeSafe(comptime .wrap("name")) orelse return false;
return std.mem.eql(u8, name_attr, self._filter);
},
.all_elements => return node._type == .element,
.child_elements => return node._type == .element,
.child_tag => {
const el = node.is(Element) orelse return false;
return el.getTag() == self._filter;
},
.selected_options => {
const el = node.is(Element) orelse return false;
const Option = Element.Html.Option;
const opt = el.is(Option) orelse return false;
return opt.getSelected();
},
.links => {
// Links are elements with href attribute (TODO: also when implemented)
const el = node.is(Element) orelse return false;
const Anchor = Element.Html.Anchor;
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe(comptime .wrap("href"));
},
.anchors => {
// Anchors are elements with name attribute
const el = node.is(Element) orelse return false;
const Anchor = Element.Html.Anchor;
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe(comptime .wrap("name"));
},
.form => {
const el = node.is(Element) orelse return false;
if (!isFormControl(el)) {
return false;
}
if (el.getAttributeSafe(comptime .wrap("form"))) |form_attr| {
const form_id = self._filter.asElement().getAttributeSafe(comptime .wrap("id")) orelse return false;
return std.mem.eql(u8, form_attr, form_id);
}
// No form attribute - match if descendant of our form
// This does an O(depth) ancestor walk for each control in the form.
//
// TODO: If profiling shows this is a bottleneck:
// When we first encounter the form element during tree walk, we could
// do a one-time reverse walk to find the LAST control that belongs to
// this form (checking both form controls and their form= attributes).
// Store that element in a new FormState. Then as we traverse
// forward:
// - Set is_within_form = true when we enter the form element
// - Return true immediately for any control while is_within_form
// - Set is_within_form = false when we reach that last element
// This trades one O(form_size) reverse walk for N O(depth) ancestor
// checks, where N = number of controls. For forms with many nested
// controls, this could be significantly faster.
return self._filter.asNode().contains(node);
},
}
}
fn isFormControl(el: *Element) bool {
if (el._type != .html) return false;
const html = el._type.html;
return switch (html._type) {
.input, .button, .select, .textarea => true,
else => false,
};
}
fn versionCheck(self: *Self, page: *const Page) bool {
const current = page.version;
if (current == self._cached_version) {
return true;
}
self._tw.reset();
self._last_index = 0;
self._last_length = null;
self._cached_version = current;
return false;
}
const HTMLCollection = @import("HTMLCollection.zig");
const NodeList = @import("NodeList.zig");
pub fn runtimeGenericWrap(self: Self, page: *Page) !if (mode == .name) *NodeList else *HTMLCollection {
const collection = switch (mode) {
.name => return page._factory.create(NodeList{ ._data = .{ .name = self } }),
.tag => HTMLCollection{ ._data = .{ .tag = self } },
.tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
.tag_name_ns => HTMLCollection{ ._data = .{ .tag_name_ns = self } },
.class_name => HTMLCollection{ ._data = .{ .class_name = self } },
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
.child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },
.selected_options => HTMLCollection{ ._data = .{ .selected_options = self } },
.links => HTMLCollection{ ._data = .{ .links = self } },
.anchors => HTMLCollection{ ._data = .{ .anchors = self } },
.form => HTMLCollection{ ._data = .{ .form = self } },
};
return page._factory.create(collection);
}
};
}
================================================
FILE: src/browser/webapi/collections.zig
================================================
// Copyright (C) 2023-2025 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 .
pub const NodeLive = @import("collections/node_live.zig").NodeLive;
pub const ChildNodes = @import("collections/ChildNodes.zig");
pub const DOMTokenList = @import("collections/DOMTokenList.zig");
pub const RadioNodeList = @import("collections/RadioNodeList.zig");
pub const HTMLCollection = @import("collections/HTMLCollection.zig");
pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig");
pub const HTMLOptionsCollection = @import("collections/HTMLOptionsCollection.zig");
pub const HTMLFormControlsCollection = @import("collections/HTMLFormControlsCollection.zig");
pub fn registerTypes() []const type {
return &.{
HTMLCollection,
HTMLCollection.Iterator,
@import("collections/NodeList.zig"),
@import("collections/NodeList.zig").KeyIterator,
@import("collections/NodeList.zig").ValueIterator,
@import("collections/NodeList.zig").EntryIterator,
@import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator,
HTMLOptionsCollection,
HTMLFormControlsCollection,
RadioNodeList,
DOMTokenList,
DOMTokenList.KeyIterator,
DOMTokenList.ValueIterator,
DOMTokenList.EntryIterator,
};
}
================================================
FILE: src/browser/webapi/css/CSSRule.zig
================================================
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CSSRule = @This();
pub const Type = enum(u16) {
style = 1,
charset = 2,
import = 3,
media = 4,
font_face = 5,
page = 6,
keyframes = 7,
keyframe = 8,
margin = 9,
namespace = 10,
counter_style = 11,
supports = 12,
document = 13,
font_feature_values = 14,
viewport = 15,
region_style = 16,
};
_type: Type,
pub fn init(rule_type: Type, page: *Page) !*CSSRule {
return page._factory.create(CSSRule{
._type = rule_type,
});
}
pub fn getType(self: *const CSSRule) u16 {
return @intFromEnum(self._type);
}
pub fn getCssText(self: *const CSSRule, page: *Page) []const u8 {
_ = self;
_ = page;
return "";
}
pub fn setCssText(self: *CSSRule, text: []const u8, page: *Page) !void {
_ = self;
_ = text;
_ = page;
}
pub fn getParentRule(self: *const CSSRule) ?*CSSRule {
_ = self;
return null;
}
pub fn getParentStyleSheet(self: *const CSSRule) ?*CSSRule {
_ = self;
return null;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSRule);
pub const Meta = struct {
pub const name = "CSSRule";
pub var class_id: bridge.ClassId = undefined;
pub const prototype_chain = bridge.prototypeChain();
};
pub const STYLE_RULE = 1;
pub const CHARSET_RULE = 2;
pub const IMPORT_RULE = 3;
pub const MEDIA_RULE = 4;
pub const FONT_FACE_RULE = 5;
pub const PAGE_RULE = 6;
pub const KEYFRAMES_RULE = 7;
pub const KEYFRAME_RULE = 8;
pub const MARGIN_RULE = 9;
pub const NAMESPACE_RULE = 10;
pub const COUNTER_STYLE_RULE = 11;
pub const SUPPORTS_RULE = 12;
pub const DOCUMENT_RULE = 13;
pub const FONT_FEATURE_VALUES_RULE = 14;
pub const VIEWPORT_RULE = 15;
pub const REGION_STYLE_RULE = 16;
pub const @"type" = bridge.accessor(CSSRule.getType, null, .{});
pub const cssText = bridge.accessor(CSSRule.getCssText, CSSRule.setCssText, .{});
pub const parentRule = bridge.accessor(CSSRule.getParentRule, null, .{});
pub const parentStyleSheet = bridge.accessor(CSSRule.getParentStyleSheet, null, .{});
};
================================================
FILE: src/browser/webapi/css/CSSRuleList.zig
================================================
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CSSRule = @import("CSSRule.zig");
const CSSRuleList = @This();
_rules: []*CSSRule = &.{},
pub fn init(page: *Page) !*CSSRuleList {
return page._factory.create(CSSRuleList{});
}
pub fn length(self: *const CSSRuleList) u32 {
return @intCast(self._rules.len);
}
pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule {
if (index >= self._rules.len) {
return null;
}
return self._rules[index];
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSRuleList);
pub const Meta = struct {
pub const name = "CSSRuleList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const length = bridge.accessor(CSSRuleList.length, null, .{});
pub const @"[]" = bridge.indexed(CSSRuleList.item, null, .{ .null_as_undefined = true });
};
================================================
FILE: src/browser/webapi/css/CSSStyleDeclaration.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const log = @import("../../../log.zig");
const String = @import("../../../string.zig").String;
const CssParser = @import("../../css/Parser.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const Allocator = std.mem.Allocator;
const CSSStyleDeclaration = @This();
_element: ?*Element = null,
_properties: std.DoublyLinkedList = .{},
_is_computed: bool = false,
pub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleDeclaration {
const self = try page._factory.create(CSSStyleDeclaration{
._element = element,
._is_computed = is_computed,
});
// Parse the element's existing style attribute into _properties so that
// subsequent JS reads and writes see all CSS properties, not just newly
// added ones. Computed styles have no inline attribute to parse.
if (!is_computed) {
if (element) |el| {
if (el.getAttributeSafe(comptime .wrap("style"))) |attr_value| {
var it = CssParser.parseDeclarationsList(attr_value);
while (it.next()) |declaration| {
try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page);
}
}
}
}
return self;
}
pub fn length(self: *const CSSStyleDeclaration) u32 {
return @intCast(self._properties.len());
}
pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
var i: u32 = 0;
var node = self._properties.first;
while (node) |n| {
if (i == index) {
const prop = Property.fromNodeLink(n);
return prop._name.str();
}
i += 1;
node = n.next;
}
return "";
}
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse {
// Only return default values for computed styles
if (self._is_computed) {
return getDefaultPropertyValue(self, normalized);
}
return "";
};
return prop._value.str();
}
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
return if (prop._important) "important" else "";
}
pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, page: *Page) !void {
// Validate priority
const priority = priority_ orelse "";
const important = if (priority.len > 0) blk: {
if (!std.ascii.eqlIgnoreCase(priority, "important")) {
return;
}
break :blk true;
} else false;
try self.setPropertyImpl(property_name, value, important, page);
try self.syncStyleAttribute(page);
}
fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, page: *Page) !void {
if (value.len == 0) {
_ = try self.removePropertyImpl(property_name, page);
return;
}
const normalized = normalizePropertyName(property_name, &page.buf);
// Normalize the value for canonical serialization
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
// Find existing property
if (self.findProperty(normalized)) |existing| {
existing._value = try String.init(page.arena, normalized_value, .{});
existing._important = important;
return;
}
// Create new property
const prop = try page._factory.create(Property{
._node = .{},
._name = try String.init(page.arena, normalized, .{}),
._value = try String.init(page.arena, normalized_value, .{}),
._important = important,
});
self._properties.append(&prop._node);
}
pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
const result = try self.removePropertyImpl(property_name, page);
try self.syncStyleAttribute(page);
return result;
}
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
// the value might not be on the heap (it could be inlined in the small string
// optimization), so we need to dupe it.
const old_value = try page.call_arena.dupe(u8, prop._value.str());
self._properties.remove(&prop._node);
page._factory.destroy(prop);
return old_value;
}
// Serialize current properties back to the element's style attribute so that
// DOM serialization (outerHTML, getAttribute) reflects JS-modified styles.
fn syncStyleAttribute(self: *CSSStyleDeclaration, page: *Page) !void {
const element = self._element orelse return;
const css_text = try self.getCssText(page);
try element.setAttributeSafe(comptime .wrap("style"), .wrap(css_text), page);
}
pub fn getFloat(self: *const CSSStyleDeclaration, page: *Page) []const u8 {
return self.getPropertyValue("float", page);
}
pub fn setFloat(self: *CSSStyleDeclaration, value_: ?[]const u8, page: *Page) !void {
try self.setPropertyImpl("float", value_ orelse "", false, page);
try self.syncStyleAttribute(page);
}
pub fn getCssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
if (self._element == null) return "";
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.format(&buf.writer);
return buf.written();
}
pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
if (self._element == null) return;
// Clear existing properties
var node = self._properties.first;
while (node) |n| {
const next = n.next;
const prop = Property.fromNodeLink(n);
self._properties.remove(n);
page._factory.destroy(prop);
node = next;
}
// Parse and set new properties
var it = CssParser.parseDeclarationsList(text);
while (it.next()) |declaration| {
try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page);
}
try self.syncStyleAttribute(page);
}
pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
const node = self._properties.first orelse return;
try Property.fromNodeLink(node).format(writer);
var next = node.next;
while (next) |n| {
try writer.writeByte(' ');
try Property.fromNodeLink(n).format(writer);
next = n.next;
}
}
fn findProperty(self: *const CSSStyleDeclaration, name: []const u8) ?*Property {
var node = self._properties.first;
while (node) |n| {
const prop = Property.fromNodeLink(n);
if (prop._name.eqlSlice(name)) {
return prop;
}
node = n.next;
}
return null;
}
fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {
if (name.len > buf.len) {
log.info(.dom, "css.long.name", .{ .name = name });
return name;
}
return std.ascii.lowerString(buf, name);
}
// Normalize CSS property values for canonical serialization
fn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: []const u8) ![]const u8 {
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
if (std.mem.eql(u8, value, "0") and isLengthProperty(property_name)) {
return "0px";
}
// "first baseline" serializes canonically as "baseline" (first is the default)
if (std.ascii.startsWithIgnoreCase(value, "first baseline")) {
if (value.len == 14) {
// Exact match "first baseline"
return "baseline";
}
if (value.len > 14 and value[14] == ' ') {
// "first baseline X" -> "baseline X"
return try std.mem.concat(arena, u8, &.{ "baseline", value[14..] });
}
}
// For 2-value shorthand properties, collapse "X X" to "X"
if (isTwoValueShorthand(property_name)) {
if (collapseDuplicateValue(value)) |single| {
return single;
}
}
// Canonicalize anchor-size() function: anchor name (dashed ident) comes before size keyword
if (std.mem.indexOf(u8, value, "anchor-size(") != null) {
return try canonicalizeAnchorSize(arena, value);
}
return value;
}
// Canonicalize anchor-size() so that the dashed ident (anchor name) comes before the size keyword.
// e.g. "anchor-size(width --foo)" -> "anchor-size(--foo width)"
fn canonicalizeAnchorSize(arena: Allocator, value: []const u8) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(arena);
var i: usize = 0;
while (i < value.len) {
// Look for "anchor-size("
if (std.mem.startsWith(u8, value[i..], "anchor-size(")) {
try buf.writer.writeAll("anchor-size(");
i += "anchor-size(".len;
// Parse and canonicalize the arguments
i = try canonicalizeAnchorSizeArgs(value, i, &buf.writer);
} else {
try buf.writer.writeByte(value[i]);
i += 1;
}
}
return buf.written();
}
// Parse anchor-size arguments and write them in canonical order
fn canonicalizeAnchorSizeArgs(value: []const u8, start: usize, writer: *std.Io.Writer) !usize {
var i = start;
var depth: usize = 1;
// Skip leading whitespace
while (i < value.len and value[i] == ' ') : (i += 1) {}
// Collect tokens before the comma or close paren
var first_token_start: ?usize = null;
var first_token_end: usize = 0;
var second_token_start: ?usize = null;
var second_token_end: usize = 0;
var comma_pos: ?usize = null;
var token_count: usize = 0;
const args_start = i;
var in_token = false;
// First pass: find the structure of arguments before comma/closing paren at depth 1
while (i < value.len and depth > 0) {
const c = value[i];
if (c == '(') {
depth += 1;
in_token = true;
i += 1;
} else if (c == ')') {
depth -= 1;
if (depth == 0) {
if (in_token) {
if (token_count == 0) {
first_token_end = i;
} else if (token_count == 1) {
second_token_end = i;
}
}
break;
}
i += 1;
} else if (c == ',' and depth == 1) {
if (in_token) {
if (token_count == 0) {
first_token_end = i;
} else if (token_count == 1) {
second_token_end = i;
}
}
comma_pos = i;
break;
} else if (c == ' ') {
if (in_token and depth == 1) {
if (token_count == 0) {
first_token_end = i;
token_count = 1;
} else if (token_count == 1 and second_token_start != null) {
second_token_end = i;
token_count = 2;
}
in_token = false;
}
i += 1;
} else {
if (!in_token and depth == 1) {
if (token_count == 0) {
first_token_start = i;
} else if (token_count == 1) {
second_token_start = i;
}
in_token = true;
}
i += 1;
}
}
// Handle end of tokens
if (in_token and token_count == 1 and second_token_start != null) {
second_token_end = i;
token_count = 2;
} else if (in_token and token_count == 0) {
first_token_end = i;
token_count = 1;
}
// Check if we have exactly two tokens that need reordering
if (token_count == 2) {
const first_start = first_token_start orelse args_start;
const second_start = second_token_start orelse first_token_end;
const first_token = value[first_start..first_token_end];
const second_token = value[second_start..second_token_end];
// If second token is a dashed ident and first is a size keyword, swap them
if (std.mem.startsWith(u8, second_token, "--") and isAnchorSizeKeyword(first_token)) {
try writer.writeAll(second_token);
try writer.writeByte(' ');
try writer.writeAll(first_token);
} else {
// Keep original order
try writer.writeAll(first_token);
try writer.writeByte(' ');
try writer.writeAll(second_token);
}
} else if (first_token_start) |fts| {
// Single token, just copy it
try writer.writeAll(value[fts..first_token_end]);
}
// Handle comma and fallback value (may contain nested anchor-size)
if (comma_pos) |cp| {
try writer.writeAll(", ");
i = cp + 1;
// Skip whitespace after comma
while (i < value.len and value[i] == ' ') : (i += 1) {}
// Copy the fallback, recursively handling nested anchor-size
while (i < value.len and depth > 0) {
if (std.mem.startsWith(u8, value[i..], "anchor-size(")) {
try writer.writeAll("anchor-size(");
i += "anchor-size(".len;
depth += 1;
i = try canonicalizeAnchorSizeArgs(value, i, writer);
depth -= 1;
} else if (value[i] == '(') {
depth += 1;
try writer.writeByte(value[i]);
i += 1;
} else if (value[i] == ')') {
depth -= 1;
if (depth == 0) break;
try writer.writeByte(value[i]);
i += 1;
} else {
try writer.writeByte(value[i]);
i += 1;
}
}
}
// Write closing paren
try writer.writeByte(')');
return i + 1; // Skip past the closing paren
}
fn isAnchorSizeKeyword(token: []const u8) bool {
const keywords = std.StaticStringMap(void).initComptime(.{
.{ "width", {} },
.{ "height", {} },
.{ "block", {} },
.{ "inline", {} },
.{ "self-block", {} },
.{ "self-inline", {} },
});
return keywords.has(token);
}
// Check if a value is "X X" (duplicate) and return just "X"
fn collapseDuplicateValue(value: []const u8) ?[]const u8 {
const space_idx = std.mem.indexOfScalar(u8, value, ' ') orelse return null;
if (space_idx == 0 or space_idx >= value.len - 1) return null;
const first = value[0..space_idx];
const rest = std.mem.trimLeft(u8, value[space_idx + 1 ..], " ");
// Check if there's only one more value (no additional spaces)
if (std.mem.indexOfScalar(u8, rest, ' ') != null) return null;
if (std.mem.eql(u8, first, rest)) {
return first;
}
return null;
}
fn isTwoValueShorthand(name: []const u8) bool {
const shorthands = std.StaticStringMap(void).initComptime(.{
.{ "place-content", {} },
.{ "place-items", {} },
.{ "place-self", {} },
.{ "margin-block", {} },
.{ "margin-inline", {} },
.{ "padding-block", {} },
.{ "padding-inline", {} },
.{ "inset-block", {} },
.{ "inset-inline", {} },
.{ "border-block-style", {} },
.{ "border-inline-style", {} },
.{ "border-block-width", {} },
.{ "border-inline-width", {} },
.{ "border-block-color", {} },
.{ "border-inline-color", {} },
.{ "overflow", {} },
.{ "overscroll-behavior", {} },
.{ "gap", {} },
.{ "grid-gap", {} },
// Scroll
.{ "scroll-padding-block", {} },
.{ "scroll-padding-inline", {} },
.{ "scroll-snap-align", {} },
// Background/Mask
.{ "background-size", {} },
.{ "border-image-repeat", {} },
.{ "mask-repeat", {} },
.{ "mask-size", {} },
});
return shorthands.has(name);
}
fn isLengthProperty(name: []const u8) bool {
// Properties that accept or values
const length_properties = std.StaticStringMap(void).initComptime(.{
// Sizing
.{ "width", {} },
.{ "height", {} },
.{ "min-width", {} },
.{ "min-height", {} },
.{ "max-width", {} },
.{ "max-height", {} },
// Margins
.{ "margin", {} },
.{ "margin-top", {} },
.{ "margin-right", {} },
.{ "margin-bottom", {} },
.{ "margin-left", {} },
.{ "margin-block", {} },
.{ "margin-block-start", {} },
.{ "margin-block-end", {} },
.{ "margin-inline", {} },
.{ "margin-inline-start", {} },
.{ "margin-inline-end", {} },
// Padding
.{ "padding", {} },
.{ "padding-top", {} },
.{ "padding-right", {} },
.{ "padding-bottom", {} },
.{ "padding-left", {} },
.{ "padding-block", {} },
.{ "padding-block-start", {} },
.{ "padding-block-end", {} },
.{ "padding-inline", {} },
.{ "padding-inline-start", {} },
.{ "padding-inline-end", {} },
// Positioning
.{ "top", {} },
.{ "right", {} },
.{ "bottom", {} },
.{ "left", {} },
.{ "inset", {} },
.{ "inset-block", {} },
.{ "inset-block-start", {} },
.{ "inset-block-end", {} },
.{ "inset-inline", {} },
.{ "inset-inline-start", {} },
.{ "inset-inline-end", {} },
// Border
.{ "border-width", {} },
.{ "border-top-width", {} },
.{ "border-right-width", {} },
.{ "border-bottom-width", {} },
.{ "border-left-width", {} },
.{ "border-block-width", {} },
.{ "border-block-start-width", {} },
.{ "border-block-end-width", {} },
.{ "border-inline-width", {} },
.{ "border-inline-start-width", {} },
.{ "border-inline-end-width", {} },
.{ "border-radius", {} },
.{ "border-top-left-radius", {} },
.{ "border-top-right-radius", {} },
.{ "border-bottom-left-radius", {} },
.{ "border-bottom-right-radius", {} },
// Text
.{ "font-size", {} },
.{ "letter-spacing", {} },
.{ "word-spacing", {} },
.{ "text-indent", {} },
// Flexbox/Grid
.{ "gap", {} },
.{ "row-gap", {} },
.{ "column-gap", {} },
.{ "flex-basis", {} },
// Legacy grid aliases
.{ "grid-column-gap", {} },
.{ "grid-row-gap", {} },
// Outline
.{ "outline", {} },
.{ "outline-width", {} },
.{ "outline-offset", {} },
// Multi-column
.{ "column-rule-width", {} },
.{ "column-width", {} },
// Scroll
.{ "scroll-margin", {} },
.{ "scroll-margin-top", {} },
.{ "scroll-margin-right", {} },
.{ "scroll-margin-bottom", {} },
.{ "scroll-margin-left", {} },
.{ "scroll-padding", {} },
.{ "scroll-padding-top", {} },
.{ "scroll-padding-right", {} },
.{ "scroll-padding-bottom", {} },
.{ "scroll-padding-left", {} },
// Shapes
.{ "shape-margin", {} },
// Motion path
.{ "offset-distance", {} },
// Transforms
.{ "translate", {} },
// Animations
.{ "animation-range-end", {} },
.{ "animation-range-start", {} },
// Other
.{ "border-spacing", {} },
.{ "text-shadow", {} },
.{ "box-shadow", {} },
.{ "baseline-shift", {} },
.{ "vertical-align", {} },
.{ "text-decoration-inset", {} },
.{ "block-step-size", {} },
// Grid lanes
.{ "flow-tolerance", {} },
.{ "column-rule-edge-inset", {} },
.{ "column-rule-interior-inset", {} },
.{ "row-rule-edge-inset", {} },
.{ "row-rule-interior-inset", {} },
.{ "rule-edge-inset", {} },
.{ "rule-interior-inset", {} },
});
return length_properties.has(name);
}
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
if (std.mem.eql(u8, normalized_name, "visibility")) {
return "visible";
}
if (std.mem.eql(u8, normalized_name, "opacity")) {
return "1";
}
if (std.mem.eql(u8, normalized_name, "display")) {
const element = self._element orelse return "";
return getDefaultDisplay(element);
}
if (std.mem.eql(u8, normalized_name, "color")) {
const element = self._element orelse return "";
return getDefaultColor(element);
}
if (std.mem.eql(u8, normalized_name, "background-color")) {
// transparent
return "rgba(0, 0, 0, 0)";
}
return "";
}
fn getDefaultDisplay(element: *const Element) []const u8 {
switch (element._type) {
.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, .legend, .map, .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";
break :blk "block";
},
};
},
.svg => return "inline",
}
}
fn isInlineTag(tag_name: []const u8) bool {
const inline_tags = [_][]const u8{
"abbr", "b", "bdi", "bdo", "cite", "code", "dfn",
"em", "i", "kbd", "mark", "q", "s", "samp",
"small", "span", "strong", "sub", "sup", "time", "u",
"var", "wbr",
};
for (inline_tags) |inline_tag| {
if (std.mem.eql(u8, tag_name, inline_tag)) {
return true;
}
}
return false;
}
fn getDefaultColor(element: *const Element) []const u8 {
switch (element._type) {
.html => |html| {
return switch (html._type) {
.anchor => "rgb(0, 0, 238)", // blue
else => "rgb(0, 0, 0)",
};
},
.svg => return "rgb(0, 0, 0)",
}
}
pub const Property = struct {
_name: String,
_value: String,
_important: bool = false,
_node: std.DoublyLinkedList.Node,
fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property {
return @alignCast(@fieldParentPtr("_node", n));
}
pub fn format(self: *const Property, writer: *std.Io.Writer) !void {
try self._name.format(writer);
try writer.writeAll(": ");
try self._value.format(writer);
if (self._important) {
try writer.writeAll(" !important");
}
try writer.writeByte(';');
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSStyleDeclaration);
pub const Meta = struct {
pub const name = "CSSStyleDeclaration";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const cssText = bridge.accessor(CSSStyleDeclaration.getCssText, CSSStyleDeclaration.setCssText, .{});
pub const length = bridge.accessor(CSSStyleDeclaration.length, null, .{});
pub const item = bridge.function(_item, .{});
fn _item(self: *const CSSStyleDeclaration, index: i32) []const u8 {
if (index < 0) {
return "";
}
return self.item(@intCast(index));
}
pub const getPropertyValue = bridge.function(CSSStyleDeclaration.getPropertyValue, .{});
pub const getPropertyPriority = bridge.function(CSSStyleDeclaration.getPropertyPriority, .{});
pub const setProperty = bridge.function(CSSStyleDeclaration.setProperty, .{});
pub const removeProperty = bridge.function(CSSStyleDeclaration.removeProperty, .{});
pub const cssFloat = bridge.accessor(CSSStyleDeclaration.getFloat, CSSStyleDeclaration.setFloat, .{});
};
const testing = @import("std").testing;
test "normalizePropertyValue: unitless zero to 0px" {
const cases = .{
.{ "width", "0", "0px" },
.{ "height", "0", "0px" },
.{ "scroll-margin-top", "0", "0px" },
.{ "scroll-padding-bottom", "0", "0px" },
.{ "column-width", "0", "0px" },
.{ "column-rule-width", "0", "0px" },
.{ "outline", "0", "0px" },
.{ "shape-margin", "0", "0px" },
.{ "offset-distance", "0", "0px" },
.{ "translate", "0", "0px" },
.{ "grid-column-gap", "0", "0px" },
.{ "grid-row-gap", "0", "0px" },
// Non-length properties should NOT normalize
.{ "opacity", "0", "0" },
.{ "z-index", "0", "0" },
};
inline for (cases) |case| {
const result = try normalizePropertyValue(testing.allocator, case[0], case[1]);
try testing.expectEqualStrings(case[2], result);
}
}
test "normalizePropertyValue: first baseline to baseline" {
const result = try normalizePropertyValue(testing.allocator, "align-items", "first baseline");
try testing.expectEqualStrings("baseline", result);
const result2 = try normalizePropertyValue(testing.allocator, "align-self", "last baseline");
try testing.expectEqualStrings("last baseline", result2);
}
test "normalizePropertyValue: collapse duplicate two-value shorthands" {
const cases = .{
.{ "overflow", "hidden hidden", "hidden" },
.{ "gap", "10px 10px", "10px" },
.{ "scroll-snap-align", "start start", "start" },
.{ "scroll-padding-block", "5px 5px", "5px" },
.{ "background-size", "auto auto", "auto" },
.{ "overscroll-behavior", "auto auto", "auto" },
// Different values should NOT collapse
.{ "overflow", "hidden scroll", "hidden scroll" },
.{ "gap", "10px 20px", "10px 20px" },
};
inline for (cases) |case| {
const result = try normalizePropertyValue(testing.allocator, case[0], case[1]);
try testing.expectEqualStrings(case[2], result);
}
}
================================================
FILE: src/browser/webapi/css/CSSStyleProperties.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Element = @import("../Element.zig");
const Page = @import("../../Page.zig");
const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig");
const CSSStyleProperties = @This();
_proto: *CSSStyleDeclaration,
pub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleProperties {
return page._factory.create(CSSStyleProperties{
._proto = try CSSStyleDeclaration.init(element, is_computed, page),
});
}
pub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {
return self._proto;
}
pub fn setNamed(self: *CSSStyleProperties, name: []const u8, value: []const u8, page: *Page) !void {
if (method_names.has(name)) {
return error.NotHandled;
}
const dash_case = camelCaseToDashCase(name, &page.buf);
try self._proto.setProperty(dash_case, value, null, page);
}
pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
if (method_names.has(name)) {
return error.NotHandled;
}
const dash_case = camelCaseToDashCase(name, &page.buf);
// Only apply vendor prefix filtering for camelCase access (no dashes in input)
// Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value
const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;
if (is_camelcase_access and std.mem.startsWith(u8, dash_case, "-")) {
// We only support -webkit-, other vendor prefixes return undefined for camelCase access
const is_webkit = std.mem.startsWith(u8, dash_case, "-webkit-");
const is_moz = std.mem.startsWith(u8, dash_case, "-moz-");
const is_ms = std.mem.startsWith(u8, dash_case, "-ms-");
const is_o = std.mem.startsWith(u8, dash_case, "-o-");
if ((is_moz or is_ms or is_o) and !is_webkit) {
return error.NotHandled;
}
}
const value = self._proto.getPropertyValue(dash_case, page);
// Property accessors have special handling for empty values:
// - Known CSS properties return '' when not set
// - Vendor-prefixed properties return undefined when not set
// - Unknown properties return undefined
if (value.len == 0) {
// Vendor-prefixed properties always return undefined when not set
if (std.mem.startsWith(u8, dash_case, "-")) {
return error.NotHandled;
}
// Known CSS properties return '', unknown properties return undefined
if (!isKnownCSSProperty(dash_case)) {
return error.NotHandled;
}
return "";
}
return value;
}
fn isKnownCSSProperty(dash_case: []const u8) bool {
const known_properties = std.StaticStringMap(void).initComptime(.{
// Colors & backgrounds
.{ "color", {} },
.{ "background", {} },
.{ "background-color", {} },
.{ "background-image", {} },
.{ "background-position", {} },
.{ "background-repeat", {} },
.{ "background-size", {} },
.{ "background-attachment", {} },
.{ "background-clip", {} },
.{ "background-origin", {} },
// Typography
.{ "font", {} },
.{ "font-family", {} },
.{ "font-size", {} },
.{ "font-style", {} },
.{ "font-weight", {} },
.{ "font-variant", {} },
.{ "line-height", {} },
.{ "letter-spacing", {} },
.{ "word-spacing", {} },
.{ "text-align", {} },
.{ "text-decoration", {} },
.{ "text-indent", {} },
.{ "text-transform", {} },
.{ "white-space", {} },
.{ "word-break", {} },
.{ "word-wrap", {} },
.{ "overflow-wrap", {} },
// Box model
.{ "margin", {} },
.{ "margin-top", {} },
.{ "margin-right", {} },
.{ "margin-bottom", {} },
.{ "margin-left", {} },
.{ "margin-block", {} },
.{ "margin-block-start", {} },
.{ "margin-block-end", {} },
.{ "margin-inline", {} },
.{ "margin-inline-start", {} },
.{ "margin-inline-end", {} },
.{ "padding", {} },
.{ "padding-top", {} },
.{ "padding-right", {} },
.{ "padding-bottom", {} },
.{ "padding-left", {} },
.{ "padding-block", {} },
.{ "padding-block-start", {} },
.{ "padding-block-end", {} },
.{ "padding-inline", {} },
.{ "padding-inline-start", {} },
.{ "padding-inline-end", {} },
// Border
.{ "border", {} },
.{ "border-width", {} },
.{ "border-style", {} },
.{ "border-color", {} },
.{ "border-top", {} },
.{ "border-top-width", {} },
.{ "border-top-style", {} },
.{ "border-top-color", {} },
.{ "border-right", {} },
.{ "border-right-width", {} },
.{ "border-right-style", {} },
.{ "border-right-color", {} },
.{ "border-bottom", {} },
.{ "border-bottom-width", {} },
.{ "border-bottom-style", {} },
.{ "border-bottom-color", {} },
.{ "border-left", {} },
.{ "border-left-width", {} },
.{ "border-left-style", {} },
.{ "border-left-color", {} },
.{ "border-radius", {} },
.{ "border-top-left-radius", {} },
.{ "border-top-right-radius", {} },
.{ "border-bottom-left-radius", {} },
.{ "border-bottom-right-radius", {} },
.{ "border-collapse", {} },
.{ "border-spacing", {} },
// Sizing
.{ "width", {} },
.{ "height", {} },
.{ "min-width", {} },
.{ "min-height", {} },
.{ "max-width", {} },
.{ "max-height", {} },
.{ "box-sizing", {} },
// Positioning
.{ "position", {} },
.{ "top", {} },
.{ "right", {} },
.{ "bottom", {} },
.{ "left", {} },
.{ "inset", {} },
.{ "inset-block", {} },
.{ "inset-block-start", {} },
.{ "inset-block-end", {} },
.{ "inset-inline", {} },
.{ "inset-inline-start", {} },
.{ "inset-inline-end", {} },
.{ "z-index", {} },
.{ "float", {} },
.{ "clear", {} },
// Display & visibility
.{ "display", {} },
.{ "visibility", {} },
.{ "opacity", {} },
.{ "overflow", {} },
.{ "overflow-x", {} },
.{ "overflow-y", {} },
.{ "clip", {} },
.{ "clip-path", {} },
// Flexbox
.{ "flex", {} },
.{ "flex-direction", {} },
.{ "flex-wrap", {} },
.{ "flex-flow", {} },
.{ "flex-grow", {} },
.{ "flex-shrink", {} },
.{ "flex-basis", {} },
.{ "order", {} },
// Grid
.{ "grid", {} },
.{ "grid-template", {} },
.{ "grid-template-columns", {} },
.{ "grid-template-rows", {} },
.{ "grid-template-areas", {} },
.{ "grid-auto-columns", {} },
.{ "grid-auto-rows", {} },
.{ "grid-auto-flow", {} },
.{ "grid-column", {} },
.{ "grid-column-start", {} },
.{ "grid-column-end", {} },
.{ "grid-row", {} },
.{ "grid-row-start", {} },
.{ "grid-row-end", {} },
.{ "grid-area", {} },
.{ "gap", {} },
.{ "row-gap", {} },
.{ "column-gap", {} },
// Alignment (flexbox & grid)
.{ "align-content", {} },
.{ "align-items", {} },
.{ "align-self", {} },
.{ "justify-content", {} },
.{ "justify-items", {} },
.{ "justify-self", {} },
.{ "place-content", {} },
.{ "place-items", {} },
.{ "place-self", {} },
// Transforms & animations
.{ "transform", {} },
.{ "transform-origin", {} },
.{ "transform-style", {} },
.{ "perspective", {} },
.{ "perspective-origin", {} },
.{ "transition", {} },
.{ "transition-property", {} },
.{ "transition-duration", {} },
.{ "transition-timing-function", {} },
.{ "transition-delay", {} },
.{ "animation", {} },
.{ "animation-name", {} },
.{ "animation-duration", {} },
.{ "animation-timing-function", {} },
.{ "animation-delay", {} },
.{ "animation-iteration-count", {} },
.{ "animation-direction", {} },
.{ "animation-fill-mode", {} },
.{ "animation-play-state", {} },
// Filters & effects
.{ "filter", {} },
.{ "backdrop-filter", {} },
.{ "box-shadow", {} },
.{ "text-shadow", {} },
// Outline
.{ "outline", {} },
.{ "outline-width", {} },
.{ "outline-style", {} },
.{ "outline-color", {} },
.{ "outline-offset", {} },
// Lists
.{ "list-style", {} },
.{ "list-style-type", {} },
.{ "list-style-position", {} },
.{ "list-style-image", {} },
// Tables
.{ "table-layout", {} },
.{ "caption-side", {} },
.{ "empty-cells", {} },
// Misc
.{ "cursor", {} },
.{ "pointer-events", {} },
.{ "user-select", {} },
.{ "resize", {} },
.{ "object-fit", {} },
.{ "object-position", {} },
.{ "vertical-align", {} },
.{ "content", {} },
.{ "quotes", {} },
.{ "counter-reset", {} },
.{ "counter-increment", {} },
// Scrolling
.{ "scroll-behavior", {} },
.{ "scroll-margin", {} },
.{ "scroll-padding", {} },
.{ "overscroll-behavior", {} },
.{ "overscroll-behavior-x", {} },
.{ "overscroll-behavior-y", {} },
// Containment
.{ "contain", {} },
.{ "container", {} },
.{ "container-type", {} },
.{ "container-name", {} },
// Aspect ratio
.{ "aspect-ratio", {} },
});
return known_properties.has(dash_case);
}
fn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 {
if (name.len == 0) {
return name;
}
// Special case: cssFloat -> float
const lower_name = std.ascii.lowerString(buf, name);
if (std.mem.eql(u8, lower_name, "cssfloat")) {
return "float";
}
// If already contains dashes, just return lowercased
if (std.mem.indexOfScalar(u8, name, '-')) |_| {
return lower_name;
}
// Check if this looks like proper camelCase (starts with lowercase)
// If not (e.g. "COLOR", "BackgroundColor"), just lowercase it
if (name.len == 0 or !std.ascii.isLower(name[0])) {
return lower_name;
}
// Check for vendor prefixes: webkitTransform -> -webkit-transform
// Must have uppercase letter after the prefix
const has_vendor_prefix = blk: {
if (name.len > 6 and std.mem.startsWith(u8, name, "webkit") and std.ascii.isUpper(name[6])) break :blk true;
if (name.len > 3 and std.mem.startsWith(u8, name, "moz") and std.ascii.isUpper(name[3])) break :blk true;
if (name.len > 2 and std.mem.startsWith(u8, name, "ms") and std.ascii.isUpper(name[2])) break :blk true;
if (name.len > 1 and std.mem.startsWith(u8, name, "o") and std.ascii.isUpper(name[1])) break :blk true;
break :blk false;
};
var write_pos: usize = 0;
if (has_vendor_prefix) {
buf[write_pos] = '-';
write_pos += 1;
}
for (name, 0..) |c, i| {
if (write_pos >= buf.len) {
return lower_name;
}
if (std.ascii.isUpper(c)) {
const skip_dash = has_vendor_prefix and i < 10 and write_pos == 1;
if (i > 0 and !skip_dash) {
if (write_pos >= buf.len) break;
buf[write_pos] = '-';
write_pos += 1;
}
if (write_pos >= buf.len) break;
buf[write_pos] = std.ascii.toLower(c);
write_pos += 1;
} else {
buf[write_pos] = c;
write_pos += 1;
}
}
return buf[0..write_pos];
}
const method_names = std.StaticStringMap(void).initComptime(.{
.{ "getPropertyValue", {} },
.{ "setProperty", {} },
.{ "removeProperty", {} },
.{ "getPropertyPriority", {} },
.{ "item", {} },
.{ "cssText", {} },
.{ "length", {} },
});
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSStyleProperties);
pub const Meta = struct {
pub const name = "CSSStyleProperties";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, CSSStyleProperties.setNamed, null, .{});
};
================================================
FILE: src/browser/webapi/css/CSSStyleRule.zig
================================================
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CSSRule = @import("CSSRule.zig");
const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig");
const CSSStyleRule = @This();
_proto: *CSSRule,
_selector_text: []const u8 = "",
_style: ?*CSSStyleDeclaration = null,
pub fn init(page: *Page) !*CSSStyleRule {
const rule = try CSSRule.init(.style, page);
return page._factory.create(CSSStyleRule{
._proto = rule,
});
}
pub fn getSelectorText(self: *const CSSStyleRule) []const u8 {
return self._selector_text;
}
pub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void {
self._selector_text = try page.dupeString(text);
}
pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration {
if (self._style) |style| {
return style;
}
const style = try CSSStyleDeclaration.init(null, false, page);
self._style = style;
return style;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSStyleRule);
pub const Meta = struct {
pub const name = "CSSStyleRule";
pub const prototype_chain = bridge.prototypeChain(CSSRule);
pub var class_id: bridge.ClassId = undefined;
};
pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});
pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{});
};
================================================
FILE: src/browser/webapi/css/CSSStyleSheet.zig
================================================
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const CSSRuleList = @import("CSSRuleList.zig");
const CSSRule = @import("CSSRule.zig");
const CSSStyleSheet = @This();
_href: ?[]const u8 = null,
_title: []const u8 = "",
_disabled: bool = false,
_css_rules: ?*CSSRuleList = null,
_owner_rule: ?*CSSRule = null,
_owner_node: ?*Element = null,
pub fn init(page: *Page) !*CSSStyleSheet {
return page._factory.create(CSSStyleSheet{});
}
pub fn initWithOwner(owner: *Element, page: *Page) !*CSSStyleSheet {
return page._factory.create(CSSStyleSheet{ ._owner_node = owner });
}
pub fn getOwnerNode(self: *const CSSStyleSheet) ?*Element {
return self._owner_node;
}
pub fn getHref(self: *const CSSStyleSheet) ?[]const u8 {
return self._href;
}
pub fn getTitle(self: *const CSSStyleSheet) []const u8 {
return self._title;
}
pub fn getDisabled(self: *const CSSStyleSheet) bool {
return self._disabled;
}
pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void {
self._disabled = disabled;
}
pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {
if (self._css_rules) |rules| return rules;
const rules = try CSSRuleList.init(page);
self._css_rules = rules;
return rules;
}
pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
return self._owner_rule;
}
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 {
_ = self;
_ = rule;
_ = index;
_ = page;
return 0;
}
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
_ = self;
_ = index;
_ = page;
}
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
_ = self;
_ = text;
// TODO: clear self.css_rules
return page.js.local.?.resolvePromise({});
}
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
_ = self;
_ = text;
// TODO: clear self.css_rules
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CSSStyleSheet);
pub const Meta = struct {
pub const name = "CSSStyleSheet";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(CSSStyleSheet.init, .{});
pub const ownerNode = bridge.accessor(CSSStyleSheet.getOwnerNode, null, .{ .null_as_undefined = true });
pub const href = bridge.accessor(CSSStyleSheet.getHref, null, .{ .null_as_undefined = true });
pub const title = bridge.accessor(CSSStyleSheet.getTitle, null, .{});
pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{});
pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{});
pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{});
pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{});
pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{});
pub const replace = bridge.function(CSSStyleSheet.replace, .{});
pub const replaceSync = bridge.function(CSSStyleSheet.replaceSync, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: CSSStyleSheet" {
try testing.htmlRunner("css/stylesheet.html", .{});
}
================================================
FILE: src/browser/webapi/css/FontFace.zig
================================================
// 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Allocator = std.mem.Allocator;
const FontFace = @This();
_arena: Allocator,
_family: []const u8,
pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {
_ = source;
const arena = try page.getArena(.{ .debug = "FontFace" });
errdefer page.releaseArena(arena);
const self = try arena.create(FontFace);
self.* = .{
._arena = arena,
._family = try arena.dupe(u8, family),
};
return self;
}
pub fn deinit(self: *FontFace, _: bool, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn getFamily(self: *const FontFace) []const u8 {
return self._family;
}
// load() - resolves immediately; headless browser has no real font loading.
pub fn load(_: *FontFace, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise({});
}
// loaded - returns an already-resolved Promise.
pub fn getLoaded(_: *FontFace, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise({});
}
pub const JsApi = struct {
pub const bridge = js.Bridge(FontFace);
pub const Meta = struct {
pub const name = "FontFace";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFace.deinit);
};
pub const constructor = bridge.constructor(FontFace.init, .{});
pub const family = bridge.accessor(FontFace.getFamily, null, .{});
pub const status = bridge.property("loaded", .{ .template = false, .readonly = true });
pub const style = bridge.property("normal", .{ .template = false, .readonly = true });
pub const weight = bridge.property("normal", .{ .template = false, .readonly = true });
pub const stretch = bridge.property("normal", .{ .template = false, .readonly = true });
pub const unicodeRange = bridge.property("U+0-10FFFF", .{ .template = false, .readonly = true });
pub const variant = bridge.property("normal", .{ .template = false, .readonly = true });
pub const featureSettings = bridge.property("normal", .{ .template = false, .readonly = true });
pub const display = bridge.property("auto", .{ .template = false, .readonly = true });
pub const loaded = bridge.accessor(FontFace.getLoaded, null, .{});
pub const load = bridge.function(FontFace.load, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: FontFace" {
try testing.htmlRunner("css/font_face.html", .{});
}
================================================
FILE: src/browser/webapi/css/FontFaceSet.zig
================================================
// 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 std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const FontFace = @import("FontFace.zig");
const EventTarget = @import("../EventTarget.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const FontFaceSet = @This();
_proto: *EventTarget,
_arena: Allocator,
pub fn init(page: *Page) !*FontFaceSet {
const arena = try page.getArena(.{ .debug = "FontFaceSet" });
errdefer page.releaseArena(arena);
return page._factory.eventTargetWithAllocator(arena, FontFaceSet{
._proto = undefined,
._arena = arena,
});
}
pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn asEventTarget(self: *FontFaceSet) *EventTarget {
return self._proto;
}
// FontFaceSet.ready - returns an already-resolved Promise.
// In a headless browser there is no font loading, so fonts are always ready.
pub fn getReady(_: *FontFaceSet, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise({});
}
// check(font, text?) - always true; headless has no real fonts to check.
pub fn check(_: *const FontFaceSet, font: []const u8) bool {
_ = font;
return true;
}
// load(font, text?) - resolves immediately with an empty array.
pub fn load(self: *FontFaceSet, font: []const u8, page: *Page) !js.Promise {
// TODO parse font to check if the font has been added before dispatching
// events.
_ = font;
// Dispatch loading event
const target = self.asEventTarget();
if (page._event_manager.hasDirectListeners(target, "loading", null)) {
const event = try Event.initTrusted(comptime .wrap("loading"), .{}, page);
try page._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
}
// Dispatch loadingdone event
if (page._event_manager.hasDirectListeners(target, "loadingdone", null)) {
const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, page);
try page._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" });
}
return page.js.local.?.resolvePromise({});
}
// add(fontFace) - no-op; headless browser does not track loaded fonts.
pub fn add(self: *FontFaceSet, _: *FontFace) *FontFaceSet {
return self;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(FontFaceSet);
pub const Meta = struct {
pub const name = "FontFaceSet";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFaceSet.deinit);
};
pub const size = bridge.property(0, .{ .template = false, .readonly = true });
pub const status = bridge.property("loaded", .{ .template = false, .readonly = true });
pub const ready = bridge.accessor(FontFaceSet.getReady, null, .{});
pub const check = bridge.function(FontFaceSet.check, .{});
pub const load = bridge.function(FontFaceSet.load, .{});
pub const add = bridge.function(FontFaceSet.add, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: FontFaceSet" {
try testing.htmlRunner("css/font_face_set.html", .{});
}
================================================
FILE: src/browser/webapi/css/MediaQueryList.zig
================================================
// Copyright (C) 2023-2025 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 .
// zlint-disable unused-decls
const std = @import("std");
const js = @import("../../js/js.zig");
const EventTarget = @import("../EventTarget.zig");
const MediaQueryList = @This();
_proto: *EventTarget,
_media: []const u8,
pub fn deinit(self: *MediaQueryList) void {
_ = self;
}
pub fn asEventTarget(self: *MediaQueryList) *EventTarget {
return self._proto;
}
pub fn getMedia(self: *const MediaQueryList) []const u8 {
return self._media;
}
pub fn addListener(_: *const MediaQueryList, _: js.Function) void {}
pub fn removeListener(_: *const MediaQueryList, _: js.Function) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(MediaQueryList);
pub const Meta = struct {
pub const name = "MediaQueryList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{});
pub const matches = bridge.property(false, .{ .template = false, .readonly = true });
pub const addListener = bridge.function(MediaQueryList.addListener, .{ .noop = true });
pub const removeListener = bridge.function(MediaQueryList.removeListener, .{ .noop = true });
};
const testing = @import("../../../testing.zig");
test "WebApi: MediaQueryList" {
try testing.htmlRunner("css/media_query_list.html", .{});
}
================================================
FILE: src/browser/webapi/css/StyleSheetList.zig
================================================
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CSSStyleSheet = @import("CSSStyleSheet.zig");
const StyleSheetList = @This();
_sheets: []*CSSStyleSheet = &.{},
pub fn init(page: *Page) !*StyleSheetList {
return page._factory.create(StyleSheetList{});
}
pub fn length(self: *const StyleSheetList) u32 {
return @intCast(self._sheets.len);
}
pub fn item(self: *const StyleSheetList, index: usize) ?*CSSStyleSheet {
if (index >= self._sheets.len) return null;
return self._sheets[index];
}
pub const JsApi = struct {
pub const bridge = js.Bridge(StyleSheetList);
pub const Meta = struct {
pub const name = "StyleSheetList";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const length = bridge.accessor(StyleSheetList.length, null, .{});
pub const @"[]" = bridge.indexed(StyleSheetList.item, null, .{ .null_as_undefined = true });
};
================================================
FILE: src/browser/webapi/element/Attribute.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const GenericIterator = @import("../collections/iterator.zig").Entry;
const Page = @import("../../Page.zig");
const String = @import("../../../string.zig").String;
const IS_DEBUG = @import("builtin").mode == .Debug;
pub fn registerTypes() []const type {
return &.{
Attribute,
NamedNodeMap,
NamedNodeMap.Iterator,
};
}
pub const Attribute = @This();
_proto: *Node,
_name: String,
_value: String,
_element: ?*Element,
pub fn format(self: *const Attribute, writer: *std.Io.Writer) !void {
return formatAttribute(self._name, self._value, writer);
}
pub fn getName(self: *const Attribute) String {
return self._name;
}
pub fn getValue(self: *const Attribute) String {
return self._value;
}
pub fn setValue(self: *Attribute, data_: ?String, page: *Page) !void {
const data = data_ orelse String.empty;
const el = self._element orelse {
self._value = try data.dupe(page.arena);
return;
};
// this takes ownership of the data
try el.setAttribute(self._name, data, page);
// not the most efficient, but we don't expect this to be called often
self._value = (try el.getAttribute(self._name, page)) orelse String.empty;
}
pub fn getNamespaceURI(_: *const Attribute) ?[]const u8 {
return null;
}
pub fn getOwnerElement(self: *const Attribute) ?*Element {
return self._element;
}
pub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool {
return self.getName().eql(other.getName()) and self.getValue().eql(other.getValue());
}
pub fn clone(self: *const Attribute, page: *Page) !*Attribute {
return page._factory.node(Attribute{
._proto = undefined,
._element = self._element,
._name = self._name,
._value = self._value,
});
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Attribute);
pub const Meta = struct {
pub const name = "Attr";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const name = bridge.accessor(Attribute.getName, null, .{});
pub const localName = bridge.accessor(Attribute.getName, null, .{});
pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{});
pub const namespaceURI = bridge.accessor(Attribute.getNamespaceURI, null, .{});
pub const ownerElement = bridge.accessor(Attribute.getOwnerElement, null, .{});
};
// This is what an Element references. It isn't exposed to JavaScript. In
// JavaScript, the element attribute list (el.attributes) is the NamedNodeMap
// which exposes Attributes. It isn't ideal that we have both.
// NamedNodeMap and Attribute are relatively fat and awkward to use. You can
// imagine a page will have tens of thousands of attributes, and it's very likely
// that page will _never_ load a single Attribute. It might get a string value
// from a string key, but it won't load the full Attribute. And, even if it does,
// it will almost certainly load realtively few.
// The main issue with Attribute is that it's a full Node -> EventTarget. It's
// _huge_ for something that's essentially just name=>value.
// That said, we need identity. el.getAttributeNode("id") should return the same
// Attribute value (the same JSValue) when called multiple time, and that gets
// more important when you look at the [hardly every used] el.removeAttributeNode
// and setAttributeNode.
// So, we maintain a lookup, page._attribute_lookup, to serve as an identity map
// from our internal Entry to a proper Attribute. This is lazily populated
// whenever an Attribute is created. Why not just have an ?*Attribute field
// in our Entry? Because that would require an extra 8 bytes for every single
// attribute in the DOM, and, again, we expect that to almost always be null.
pub const List = struct {
normalize: bool,
/// Length of items in `_list`. Not usize to increase memory usage.
/// Honestly, this is more than enough.
_len: u32 = 0,
_list: std.DoublyLinkedList = .{},
pub fn isEmpty(self: *const List) bool {
return self._list.first == null;
}
pub fn get(self: *const List, name: String, page: *Page) !?String {
const entry = (try self.getEntry(name, page)) orelse return null;
return entry._value;
}
pub inline fn length(self: *const List) usize {
return self._len;
}
/// Compares 2 attribute lists for equality.
pub fn eql(self: *List, other: *List) bool {
if (self.length() != other.length()) {
return false;
}
var iter = self.iterator();
search: while (iter.next()) |attr| {
// Iterate over all `other` attributes.
var other_iter = other.iterator();
while (other_iter.next()) |other_attr| {
if (attr.eql(other_attr)) {
continue :search; // Found match.
}
}
// Iterated over all `other` and not match.
return false;
}
return true;
}
// meant for internal usage, where the name is known to be properly cased
pub fn getSafe(self: *const List, name: String) ?[]const u8 {
const entry = self.getEntryWithNormalizedName(name) orelse return null;
return entry._value.str();
}
// meant for internal usage, where the name is known to be properly cased
pub fn hasSafe(self: *const List, name: String) bool {
return self.getEntryWithNormalizedName(name) != null;
}
pub fn getAttribute(self: *const List, name: String, element: ?*Element, page: *Page) !?*Attribute {
const entry = (try self.getEntry(name, page)) orelse return null;
const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry));
if (gop.found_existing) {
return gop.value_ptr.*;
}
const attribute = try entry.toAttribute(element, page);
gop.value_ptr.* = attribute;
return attribute;
}
pub fn put(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {
const result = try self.getEntryAndNormalizedName(name, page);
return self._put(result, value, element, page);
}
pub fn putSafe(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {
const entry = self.getEntryWithNormalizedName(name);
return self._put(.{ .entry = entry, .normalized = name }, value, element, page);
}
fn _put(self: *List, result: NormalizeAndEntry, value: String, element: *Element, page: *Page) !*Entry {
const is_id = shouldAddToIdMap(result.normalized, element);
var entry: *Entry = undefined;
var old_value: ?String = null;
if (result.entry) |e| {
old_value = try e._value.dupe(page.call_arena);
if (is_id) {
page.removeElementId(element, e._value.str());
}
e._value = try value.dupe(page.arena);
entry = e;
} else {
entry = try page._factory.create(Entry{
._node = .{},
._name = try result.normalized.dupe(page.arena),
._value = try value.dupe(page.arena),
});
self._list.append(&entry._node);
self._len += 1;
}
if (is_id) {
const parent = element.asNode()._parent orelse {
return entry;
};
try page.addElementId(parent, element, entry._value.str());
}
page.domChanged();
page.attributeChange(element, result.normalized, entry._value, old_value);
return entry;
}
// Optimized for cloning. We know `name` is already normalized. We know there isn't duplicates.
// We know the Element is detatched (and thus, don't need to check for `id`).
pub fn putForCloned(self: *List, name: []const u8, value: []const u8, page: *Page) !void {
const entry = try page._factory.create(Entry{
._node = .{},
._name = try String.init(page.arena, name, .{}),
._value = try String.init(page.arena, value, .{}),
});
self._list.append(&entry._node);
self._len += 1;
}
// not efficient, won't be called often (if ever!)
pub fn putAttribute(self: *List, attribute: *Attribute, element: *Element, page: *Page) !?*Attribute {
// we expect our caller to make sure this is true
if (comptime IS_DEBUG) {
std.debug.assert(attribute._element == null);
}
const existing_attribute = try self.getAttribute(attribute._name, element, page);
if (existing_attribute) |ea| {
try self.delete(ea._name, element, page);
}
const entry = try self.put(attribute._name, attribute._value, element, page);
attribute._element = element;
try page._attribute_lookup.put(page.arena, @intFromPtr(entry), attribute);
return existing_attribute;
}
// called form our parser, names already lower-cased
pub fn putNew(self: *List, name: []const u8, value: []const u8, page: *Page) !void {
if (try self.getEntry(.wrap(name), page) != null) {
// When parsing, if there are dupicate names, it isn't valid, and
// the first is kept
return;
}
const entry = try page._factory.create(Entry{
._node = .{},
._name = try String.init(page.arena, name, .{}),
._value = try String.init(page.arena, value, .{}),
});
self._list.append(&entry._node);
self._len += 1;
}
pub fn delete(self: *List, name: String, element: *Element, page: *Page) !void {
const result = try self.getEntryAndNormalizedName(name, page);
const entry = result.entry orelse return;
const is_id = shouldAddToIdMap(result.normalized, element);
const old_value = entry._value;
if (is_id) {
page.removeElementId(element, entry._value.str());
}
page.domChanged();
page.attributeRemove(element, result.normalized, old_value);
_ = page._attribute_lookup.remove(@intFromPtr(entry));
self._list.remove(&entry._node);
self._len -= 1;
page._factory.destroy(entry);
}
pub fn getNames(self: *const List, page: *Page) ![][]const u8 {
var arr: std.ArrayList([]const u8) = .empty;
var node = self._list.first;
while (node) |n| {
try arr.append(page.call_arena, Entry.fromNode(n)._name.str());
node = n.next;
}
return arr.items;
}
pub fn iterator(self: *List) InnerIterator {
return .{ ._node = self._list.first };
}
fn getEntry(self: *const List, name: String, page: *Page) !?*Entry {
const result = try self.getEntryAndNormalizedName(name, page);
return result.entry;
}
// Dangerous, the returned normalized name is only valid until someone
// else uses pages.buf.
const NormalizeAndEntry = struct {
entry: ?*Entry,
normalized: String,
};
fn getEntryAndNormalizedName(self: *const List, name: String, page: *Page) !NormalizeAndEntry {
const normalized =
if (self.normalize) try normalizeNameForLookup(name, page) else name;
return .{
.normalized = normalized,
.entry = self.getEntryWithNormalizedName(normalized),
};
}
fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {
var node = self._list.first;
while (node) |n| {
var e = Entry.fromNode(n);
if (e._name.eql(name)) {
return e;
}
node = n.next;
}
return null;
}
pub const Entry = struct {
_name: String,
_value: String,
_node: std.DoublyLinkedList.Node,
fn fromNode(n: *std.DoublyLinkedList.Node) *Entry {
return @alignCast(@fieldParentPtr("_node", n));
}
/// Returns true if 2 entries are equal.
/// This doesn't compare `_node` fields.
pub fn eql(self: *const Entry, other: *const Entry) bool {
return self._name.eql(other._name) and self._value.eql(other._value);
}
pub fn format(self: *const Entry, writer: *std.Io.Writer) !void {
return formatAttribute(self._name, self._value, writer);
}
pub fn toAttribute(self: *const Entry, element: ?*Element, page: *Page) !*Attribute {
return page._factory.node(Attribute{
._proto = undefined,
._element = element,
// Cannot directly reference self._name.str() and self._value.str()
// This attribute can outlive the list entry (the node can be
// removed from the element's attribute, but still exist in the DOM)
._name = try self._name.dupe(page.arena),
._value = try self._value.dupe(page.arena),
});
}
};
};
fn shouldAddToIdMap(normalized_name: String, element: *Element) bool {
if (!normalized_name.eql(comptime .wrap("id"))) {
return false;
}
const node = element.asNode();
// Shadow tree elements are always added to their shadow root's map
if (node.isInShadowTree()) {
return true;
}
// Document tree elements only when connected
return node.isConnected();
}
pub fn validateAttributeName(name: String) !void {
const name_str = name.str();
if (name_str.len == 0) {
return error.InvalidCharacterError;
}
const first = name_str[0];
if ((first >= '0' and first <= '9') or first == '-' or first == '.') {
return error.InvalidCharacterError;
}
for (name_str) |c| {
if (c == 0 or c == '/' or c == '=' or c == '>' or std.ascii.isWhitespace(c)) {
return error.InvalidCharacterError;
}
const is_valid = (c >= 'a' and c <= 'z') or
(c >= 'A' and c <= 'Z') or
(c >= '0' and c <= '9') or
c == '_' or c == '-' or c == '.' or c == ':';
if (!is_valid) {
return error.InvalidCharacterError;
}
}
}
pub fn normalizeNameForLookup(name: String, page: *Page) !String {
if (!needsLowerCasing(name.str())) {
return name;
}
const normalized = if (name.len < page.buf.len)
std.ascii.lowerString(&page.buf, name.str())
else
try std.ascii.allocLowerString(page.call_arena, name.str());
return .wrap(normalized);
}
fn needsLowerCasing(name: []const u8) bool {
var remaining = name;
if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {
while (remaining.len > vector_len) {
const chunk: @Vector(vector_len, u8) = remaining[0..vector_len].*;
if (@reduce(.Min, chunk) <= 'Z') {
return true;
}
remaining = remaining[vector_len..];
}
}
for (remaining) |b| {
if (std.ascii.isUpper(b)) {
return true;
}
}
return false;
}
pub const NamedNodeMap = struct {
_list: *List,
// Whenever the NamedNodeMap creates an Attribute, it needs to provide the
// "ownerElement".
_element: *Element,
pub fn length(self: *const NamedNodeMap) u32 {
return @intCast(self._list._list.len());
}
pub fn getAtIndex(self: *const NamedNodeMap, index: usize, page: *Page) !?*Attribute {
var i: usize = 0;
var node = self._list._list.first;
while (node) |n| {
if (i == index) {
var entry = List.Entry.fromNode(n);
const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry));
if (gop.found_existing) {
return gop.value_ptr.*;
}
const attribute = try entry.toAttribute(self._element, page);
gop.value_ptr.* = attribute;
return attribute;
}
node = n.next;
i += 1;
}
return null;
}
pub fn getByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {
return self._list.getAttribute(name, self._element, page);
}
pub fn set(self: *const NamedNodeMap, attribute: *Attribute, page: *Page) !?*Attribute {
attribute._element = null; // just a requirement of list.putAttribute, it'll re-set it.
return self._list.putAttribute(attribute, self._element, page);
}
pub fn removeByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {
// this 2-step process (get then delete) isn't efficient. But we don't
// expect this to be called often, and this lets us keep delete straightforward.
const attr = (try self.getByName(name, page)) orelse return null;
try self._list.delete(name, self._element, page);
return attr;
}
pub fn iterator(self: *const NamedNodeMap, page: *Page) !*Iterator {
return .init(.{ .list = self }, page);
}
pub const Iterator = GenericIterator(struct {
index: usize = 0,
list: *const NamedNodeMap,
pub fn next(self: *@This(), page: *Page) !?*Attribute {
const index = self.index;
self.index = index + 1;
return self.list.getAtIndex(index, page);
}
}, null);
pub const JsApi = struct {
pub const bridge = js.Bridge(NamedNodeMap);
pub const Meta = struct {
pub const name = "NamedNodeMap";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const length = bridge.accessor(NamedNodeMap.length, null, .{});
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{});
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});
pub const item = bridge.function(_item, .{});
fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute {
// the bridge.indexed handles this, so if we want
// list.item(-2) to return the same as list[-2] we need to
// 1 - take an i32 for the index
// 2 - return null if it's < 0
if (index < 0) {
return null;
}
return self.getAtIndex(@intCast(index), page);
}
pub const symbol_iterator = bridge.iterator(NamedNodeMap.iterator, .{});
};
};
// Not meant to be exposed. The "public" iterator is a NamedNodeMap, and it's a
// bit awkward. Having this for more straightforward key=>value is useful for
// the few internal places we need to iterate through the attributes (e.g. dump)
pub const InnerIterator = struct {
_node: ?*std.DoublyLinkedList.Node = null,
pub fn next(self: *InnerIterator) ?*List.Entry {
const node = self._node orelse return null;
self._node = node.next;
return List.Entry.fromNode(node);
}
};
fn formatAttribute(name: String, value_: String, writer: *std.Io.Writer) !void {
try writer.writeAll(name.str());
// Boolean attributes with empty values are serialized without a value
const value = value_.str();
if (value.len == 0 and boolean_attributes_lookup.has(name.str())) {
return;
}
try writer.writeByte('=');
if (value.len == 0) {
return writer.writeAll("\"\"");
}
try writer.writeByte('"');
const offset = std.mem.indexOfAny(u8, value, "`' &\"<>=") orelse {
try writer.writeAll(value);
return writer.writeByte('"');
};
try writeEscapedAttributeValue(value, offset, writer);
return writer.writeByte('"');
}
const boolean_attributes = [_][]const u8{
"checked",
"disabled",
"required",
"readonly",
"multiple",
"selected",
"autofocus",
"autoplay",
"controls",
"loop",
"muted",
"hidden",
"async",
"defer",
"novalidate",
"formnovalidate",
"ismap",
"reversed",
"default",
"open",
};
const boolean_attributes_lookup = std.StaticStringMap(void).initComptime(blk: {
var entries: [boolean_attributes.len]struct { []const u8, void } = undefined;
for (boolean_attributes, 0..) |attr, i| {
entries[i] = .{ attr, {} };
}
break :blk entries;
});
fn writeEscapedAttributeValue(value: []const u8, first_offset: usize, writer: *std.Io.Writer) !void {
// Write everything before the first special character
try writer.writeAll(value[0..first_offset]);
try writer.writeAll(switch (value[first_offset]) {
'&' => "&",
'"' => """,
'<' => "<",
'>' => ">",
'=' => "=",
' ' => " ",
'`' => "`",
'\'' => "'",
else => unreachable,
});
var remaining = value[first_offset + 1 ..];
while (std.mem.indexOfAny(u8, remaining, "&\"<>")) |offset| {
try writer.writeAll(remaining[0..offset]);
try writer.writeAll(switch (remaining[offset]) {
'&' => "&",
'"' => """,
'<' => "<",
'>' => ">",
else => unreachable,
});
remaining = remaining[offset + 1 ..];
}
if (remaining.len > 0) {
try writer.writeAll(remaining);
}
}
================================================
FILE: src/browser/webapi/element/DOMStringMap.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../js/js.zig");
const Element = @import("../Element.zig");
const Page = @import("../../Page.zig");
const String = @import("../../../string.zig").String;
const Allocator = std.mem.Allocator;
const DOMStringMap = @This();
_element: *Element,
fn getProperty(self: *DOMStringMap, name: String, page: *Page) !?String {
const attr_name = try camelToKebab(page.call_arena, name);
return try self._element.getAttribute(attr_name, page);
}
fn setProperty(self: *DOMStringMap, name: String, value: String, page: *Page) !void {
const attr_name = try camelToKebab(page.call_arena, name);
return self._element.setAttributeSafe(attr_name, value, page);
}
fn deleteProperty(self: *DOMStringMap, name: String, page: *Page) !void {
const attr_name = try camelToKebab(page.call_arena, name);
try self._element.removeAttribute(attr_name, page);
}
// fooBar -> data-foo-bar (with SSO optimization for short strings)
fn camelToKebab(arena: Allocator, camel: String) !String {
const camel_str = camel.str();
// Calculate output length
var output_len: usize = 5; // "data-"
for (camel_str, 0..) |c, i| {
output_len += 1;
if (std.ascii.isUpper(c) and i > 0) output_len += 1; // extra char for '-'
}
if (output_len <= 12) {
// SSO path - no allocation!
var content: [12]u8 = @splat(0);
@memcpy(content[0..5], "data-");
var idx: usize = 5;
for (camel_str, 0..) |c, i| {
if (std.ascii.isUpper(c)) {
if (i > 0) {
content[idx] = '-';
idx += 1;
}
content[idx] = std.ascii.toLower(c);
} else {
content[idx] = c;
}
idx += 1;
}
return .{ .len = @intCast(output_len), .payload = .{ .content = content } };
}
// Fallback: allocate for longer strings
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(arena, output_len);
result.appendSliceAssumeCapacity("data-");
for (camel_str, 0..) |c, i| {
if (std.ascii.isUpper(c)) {
if (i > 0) {
result.appendAssumeCapacity('-');
}
result.appendAssumeCapacity(std.ascii.toLower(c));
} else {
result.appendAssumeCapacity(c);
}
}
return try String.init(arena, result.items, .{});
}
// data-foo-bar -> fooBar
fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 {
if (!std.mem.startsWith(u8, kebab, "data-")) {
return null;
}
const data_part = kebab[5..]; // Skip "data-"
if (data_part.len == 0) {
return null;
}
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(arena, data_part.len);
var capitalize_next = false;
for (data_part) |c| {
if (c == '-') {
capitalize_next = true;
} else if (capitalize_next) {
result.appendAssumeCapacity(std.ascii.toUpper(c));
capitalize_next = false;
} else {
result.appendAssumeCapacity(c);
}
}
return result.items;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMStringMap);
pub const Meta = struct {
pub const name = "DOMStringMap";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const @"[]" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, .{ .null_as_undefined = true });
};
================================================
FILE: src/browser/webapi/element/Html.zig
================================================
// 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 std = @import("std");
const js = @import("../../js/js.zig");
const reflect = @import("../../reflect.zig");
const log = @import("../../../log.zig");
const global_event_handlers = @import("../global_event_handlers.zig");
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
const GlobalEventHandler = global_event_handlers.Handler;
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
pub const Anchor = @import("html/Anchor.zig");
pub const Area = @import("html/Area.zig");
pub const Base = @import("html/Base.zig");
pub const Body = @import("html/Body.zig");
pub const BR = @import("html/BR.zig");
pub const Button = @import("html/Button.zig");
pub const Canvas = @import("html/Canvas.zig");
pub const Custom = @import("html/Custom.zig");
pub const Data = @import("html/Data.zig");
pub const DataList = @import("html/DataList.zig");
pub const Details = @import("html/Details.zig");
pub const Dialog = @import("html/Dialog.zig");
pub const Directory = @import("html/Directory.zig");
pub const Div = @import("html/Div.zig");
pub const DList = @import("html/DList.zig");
pub const Embed = @import("html/Embed.zig");
pub const FieldSet = @import("html/FieldSet.zig");
pub const Font = @import("html/Font.zig");
pub const Form = @import("html/Form.zig");
pub const Generic = @import("html/Generic.zig");
pub const Head = @import("html/Head.zig");
pub const Heading = @import("html/Heading.zig");
pub const HR = @import("html/HR.zig");
pub const Html = @import("html/Html.zig");
pub const IFrame = @import("html/IFrame.zig");
pub const Image = @import("html/Image.zig");
pub const Input = @import("html/Input.zig");
pub const Label = @import("html/Label.zig");
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 Media = @import("html/Media.zig");
pub const Meta = @import("html/Meta.zig");
pub const Meter = @import("html/Meter.zig");
pub const Mod = @import("html/Mod.zig");
pub const Object = @import("html/Object.zig");
pub const OL = @import("html/OL.zig");
pub const OptGroup = @import("html/OptGroup.zig");
pub const Option = @import("html/Option.zig");
pub const Output = @import("html/Output.zig");
pub const Paragraph = @import("html/Paragraph.zig");
pub const Picture = @import("html/Picture.zig");
pub const Param = @import("html/Param.zig");
pub const Pre = @import("html/Pre.zig");
pub const Progress = @import("html/Progress.zig");
pub const Quote = @import("html/Quote.zig");
pub const Script = @import("html/Script.zig");
pub const Select = @import("html/Select.zig");
pub const Slot = @import("html/Slot.zig");
pub const Source = @import("html/Source.zig");
pub const Span = @import("html/Span.zig");
pub const Style = @import("html/Style.zig");
pub const Table = @import("html/Table.zig");
pub const TableCaption = @import("html/TableCaption.zig");
pub const TableCell = @import("html/TableCell.zig");
pub const TableCol = @import("html/TableCol.zig");
pub const TableRow = @import("html/TableRow.zig");
pub const TableSection = @import("html/TableSection.zig");
pub const Template = @import("html/Template.zig");
pub const TextArea = @import("html/TextArea.zig");
pub const Time = @import("html/Time.zig");
pub const Title = @import("html/Title.zig");
pub const Track = @import("html/Track.zig");
pub const UL = @import("html/UL.zig");
pub const Unknown = @import("html/Unknown.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const HtmlElement = @This();
_type: Type,
_proto: *Element,
// Special constructor for custom elements
pub fn construct(page: *Page) !*Element {
const node = page._upgrading_element orelse return error.IllegalConstructor;
return node.is(Element) orelse return error.IllegalConstructor;
}
pub const Type = union(enum) {
anchor: *Anchor,
area: *Area,
base: *Base,
body: *Body,
br: *BR,
button: *Button,
canvas: *Canvas,
custom: *Custom,
data: *Data,
datalist: *DataList,
details: *Details,
dialog: *Dialog,
directory: *Directory,
div: *Div,
dl: *DList,
embed: *Embed,
fieldset: *FieldSet,
font: *Font,
form: *Form,
generic: *Generic,
heading: *Heading,
head: *Head,
html: *Html,
hr: *HR,
img: *Image,
iframe: *IFrame,
input: *Input,
label: *Label,
legend: *Legend,
li: *LI,
link: *Link,
map: *Map,
media: *Media,
meta: *Meta,
meter: *Meter,
mod: *Mod,
object: *Object,
ol: *OL,
optgroup: *OptGroup,
option: *Option,
output: *Output,
p: *Paragraph,
picture: *Picture,
param: *Param,
pre: *Pre,
progress: *Progress,
quote: *Quote,
script: *Script,
select: *Select,
slot: *Slot,
source: *Source,
span: *Span,
style: *Style,
table: *Table,
table_caption: *TableCaption,
table_cell: *TableCell,
table_col: *TableCol,
table_row: *TableRow,
table_section: *TableSection,
template: *Template,
textarea: *TextArea,
time: *Time,
title: *Title,
track: *Track,
ul: *UL,
unknown: *Unknown,
};
pub fn is(self: *HtmlElement, comptime T: type) ?*T {
inline for (@typeInfo(Type).@"union".fields) |f| {
if (@field(Type, f.name) == self._type) {
if (f.type == T) {
return &@field(self._type, f.name);
}
if (f.type == *T) {
return @field(self._type, f.name);
}
}
}
return null;
}
pub fn asElement(self: *HtmlElement) *Element {
return self._proto;
}
pub fn asNode(self: *HtmlElement) *Node {
return self._proto._proto;
}
pub fn asEventTarget(self: *HtmlElement) *@import("../EventTarget.zig") {
return self._proto._proto._proto;
}
// innerText represents the **rendered** text content of a node and its
// descendants.
pub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer) !void {
var state = innerTextState{};
return try self._getInnerText(writer, &state);
}
const innerTextState = struct {
pre_w: bool = false,
trim_left: bool = true,
};
fn _getInnerText(self: *HtmlElement, writer: *std.Io.Writer, state: *innerTextState) !void {
var it = self.asElement().asNode().childrenIterator();
while (it.next()) |child| {
switch (child._type) {
.element => |e| switch (e._type) {
.html => |he| switch (he._type) {
.br => {
try writer.writeByte('\n');
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.script, .style, .template => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
else => try he._getInnerText(writer, state), // TODO check if elt is hidden.
},
.svg => {},
},
.cdata => |c| switch (c._type) {
.comment => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.text => {
if (state.pre_w) try writer.writeByte(' ');
state.pre_w = try c.render(writer, .{ .trim_left = state.trim_left });
// if we had a pre space, trim left next one.
state.trim_left = state.pre_w;
},
// CDATA sections should not be used within HTML. They are
// considered comments and are not displayed.
.cdata_section => {},
// Processing instructions are not displayed in innerText
.processing_instruction => {},
},
.document => {},
.document_type => {},
.document_fragment => {},
.attribute => |attr| try writer.writeAll(attr._value.str()),
}
}
}
pub fn setInnerText(self: *HtmlElement, text: []const u8, page: *Page) !void {
const parent = self.asElement().asNode();
// Remove all existing children
page.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Fast path: skip if text is empty
if (text.len == 0) {
return;
}
// Create and append text node
const text_node = try page.createTextNode(text);
try page.appendNode(parent, text_node, .{ .child_already_connected = false });
}
pub fn insertAdjacentHTML(
self: *HtmlElement,
position: []const u8,
html: []const u8,
page: *Page,
) !void {
// Create a new HTMLDocument.
const doc = try page._factory.document(@import("../HTMLDocument.zig"){
._proto = undefined,
});
const doc_node = doc.asNode();
const arena = try page.getArena(.{ .debug = "HTML.insertAdjacentHTML" });
defer page.releaseArena(arena);
const Parser = @import("../../parser/Parser.zig");
var parser = Parser.init(arena, doc_node, page);
parser.parse(html);
// Check if there's parsing error.
if (parser.err) |_| {
return error.Invalid;
}
// The parser wraps content in a document structure:
// - Typical: ......
// - Head-only: (no body)
// - Empty/comments: May have no element at all
const html_node = doc_node.firstChild() orelse return;
const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);
// Iterate through all children of (typically and/or )
// and insert their children (not the containers themselves) into the target.
// This handles both body content AND head-only elements like , , etc.
var html_children = html_node.childrenIterator();
while (html_children.next()) |container| {
var iter = container.childrenIterator();
while (iter.next()) |child_node| {
_ = try target_node.insertBefore(child_node, prev_node, page);
}
}
}
pub fn click(self: *HtmlElement, page: *Page) !void {
switch (self._type) {
inline .button, .input, .textarea, .select => |i| {
if (i.getDisabled()) {
return;
}
},
else => {},
}
const event = (try @import("../event/MouseEvent.zig").init("click", .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = 0,
.clientY = 0,
}, page)).asEvent();
try page._event_manager.dispatch(self.asEventTarget(), event);
}
// TODO: Per spec, hidden is a tristate: true | false | "until-found".
// We only support boolean for now; "until-found" would need bridge union support.
pub fn getHidden(self: *HtmlElement) bool {
return self.asElement().getAttributeSafe(comptime .wrap("hidden")) != null;
}
pub fn setHidden(self: *HtmlElement, hidden: bool, page: *Page) !void {
if (hidden) {
try self.asElement().setAttributeSafe(comptime .wrap("hidden"), .wrap(""), page);
} else {
try self.asElement().removeAttribute(comptime .wrap("hidden"), page);
}
}
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,
};
};
return std.fmt.parseInt(i32, attr, 10) catch -1;
}
pub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void {
var buf: [12]u8 = undefined;
const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch unreachable;
try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page);
}
pub fn getAttributeFunction(
self: *HtmlElement,
listener_type: GlobalEventHandler,
page: *Page,
) !?js.Function.Global {
const element = self.asElement();
if (page._event_target_attr_listeners.get(.{ .target = element.asEventTarget(), .handler = listener_type })) |cached| {
return cached;
}
const attr = element.getAttributeSafe(.wrap(@tagName(listener_type))) orelse return null;
const function = page.js.stringToPersistedFunction(attr, &.{"event"}, &.{}) catch |err| {
// Not a valid expression; log this to find out if its something we should be supporting.
log.warn(.js, "Html.getAttributeFunction", .{
.expression = attr,
.err = err,
});
return null;
};
try self.setAttributeListener(listener_type, function, page);
return function;
}
pub fn hasAttributeFunction(self: *HtmlElement, listener_type: GlobalEventHandler, page: *const Page) bool {
return page._event_target_attr_listeners.contains(.{ .target = self.asEventTarget(), .handler = listener_type });
}
fn setAttributeListener(
self: *Element.Html,
listener_type: GlobalEventHandler,
listener_callback: ?js.Function.Global,
page: *Page,
) !void {
if (comptime IS_DEBUG) {
log.debug(.event, "Html.setAttributeListener", .{
.type = std.meta.activeTag(self._type),
.listener_type = listener_type,
});
}
if (listener_callback) |cb| {
try page._event_target_attr_listeners.put(page.arena, .{
.target = self.asEventTarget(),
.handler = listener_type,
}, cb);
return;
}
// The listener is null, remove existing listener.
_ = page._event_target_attr_listeners.remove(.{
.target = self.asEventTarget(),
.handler = listener_type,
});
}
pub fn setOnAbort(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onabort, callback, page);
}
pub fn getOnAbort(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onabort, page);
}
pub fn setOnAnimationCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onanimationcancel, callback, page);
}
pub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onanimationcancel, page);
}
pub fn setOnAnimationEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onanimationend, callback, page);
}
pub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onanimationend, page);
}
pub fn setOnAnimationIteration(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onanimationiteration, callback, page);
}
pub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onanimationiteration, page);
}
pub fn setOnAnimationStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onanimationstart, callback, page);
}
pub fn getOnAnimationStart(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onanimationstart, page);
}
pub fn setOnAuxClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onauxclick, callback, page);
}
pub fn getOnAuxClick(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onauxclick, page);
}
pub fn setOnBeforeInput(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onbeforeinput, callback, page);
}
pub fn getOnBeforeInput(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onbeforeinput, page);
}
pub fn setOnBeforeMatch(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onbeforematch, callback, page);
}
pub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onbeforematch, page);
}
pub fn setOnBeforeToggle(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onbeforetoggle, callback, page);
}
pub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onbeforetoggle, page);
}
pub fn setOnBlur(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onblur, callback, page);
}
pub fn getOnBlur(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onblur, page);
}
pub fn setOnCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncancel, callback, page);
}
pub fn getOnCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncancel, page);
}
pub fn setOnCanPlay(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncanplay, callback, page);
}
pub fn getOnCanPlay(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncanplay, page);
}
pub fn setOnCanPlayThrough(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncanplaythrough, callback, page);
}
pub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncanplaythrough, page);
}
pub fn setOnChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onchange, callback, page);
}
pub fn getOnChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onchange, page);
}
pub fn setOnClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onclick, callback, page);
}
pub fn getOnClick(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onclick, page);
}
pub fn setOnClose(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onclose, callback, page);
}
pub fn getOnClose(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onclose, page);
}
pub fn setOnCommand(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncommand, callback, page);
}
pub fn getOnCommand(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncommand, page);
}
pub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncontentvisibilityautostatechange, callback, page);
}
pub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncontentvisibilityautostatechange, page);
}
pub fn setOnContextLost(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncontextlost, callback, page);
}
pub fn getOnContextLost(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncontextlost, page);
}
pub fn setOnContextMenu(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncontextmenu, callback, page);
}
pub fn getOnContextMenu(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncontextmenu, page);
}
pub fn setOnContextRestored(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncontextrestored, callback, page);
}
pub fn getOnContextRestored(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncontextrestored, page);
}
pub fn setOnCopy(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncopy, callback, page);
}
pub fn getOnCopy(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncopy, page);
}
pub fn setOnCueChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncuechange, callback, page);
}
pub fn getOnCueChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncuechange, page);
}
pub fn setOnCut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oncut, callback, page);
}
pub fn getOnCut(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oncut, page);
}
pub fn setOnDblClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondblclick, callback, page);
}
pub fn getOnDblClick(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondblclick, page);
}
pub fn setOnDrag(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondrag, callback, page);
}
pub fn getOnDrag(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondrag, page);
}
pub fn setOnDragEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragend, callback, page);
}
pub fn getOnDragEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragend, page);
}
pub fn setOnDragEnter(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragenter, callback, page);
}
pub fn getOnDragEnter(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragenter, page);
}
pub fn setOnDragExit(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragexit, callback, page);
}
pub fn getOnDragExit(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragexit, page);
}
pub fn setOnDragLeave(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragleave, callback, page);
}
pub fn getOnDragLeave(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragleave, page);
}
pub fn setOnDragOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragover, callback, page);
}
pub fn getOnDragOver(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragover, page);
}
pub fn setOnDragStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondragstart, callback, page);
}
pub fn getOnDragStart(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondragstart, page);
}
pub fn setOnDrop(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondrop, callback, page);
}
pub fn getOnDrop(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondrop, page);
}
pub fn setOnDurationChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ondurationchange, callback, page);
}
pub fn getOnDurationChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ondurationchange, page);
}
pub fn setOnEmptied(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onemptied, callback, page);
}
pub fn getOnEmptied(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onemptied, page);
}
pub fn setOnEnded(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onended, callback, page);
}
pub fn getOnEnded(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onended, page);
}
pub fn setOnError(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onerror, callback, page);
}
pub fn getOnError(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onerror, page);
}
pub fn setOnFocus(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onfocus, callback, page);
}
pub fn getOnFocus(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onfocus, page);
}
pub fn setOnFormData(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onformdata, callback, page);
}
pub fn getOnFormData(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onformdata, page);
}
pub fn setOnFullscreenChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onfullscreenchange, callback, page);
}
pub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onfullscreenchange, page);
}
pub fn setOnFullscreenError(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onfullscreenerror, callback, page);
}
pub fn getOnFullscreenError(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onfullscreenerror, page);
}
pub fn setOnGotPointerCapture(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ongotpointercapture, callback, page);
}
pub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ongotpointercapture, page);
}
pub fn setOnInput(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oninput, callback, page);
}
pub fn getOnInput(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oninput, page);
}
pub fn setOnInvalid(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.oninvalid, callback, page);
}
pub fn getOnInvalid(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.oninvalid, page);
}
pub fn setOnKeyDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onkeydown, callback, page);
}
pub fn getOnKeyDown(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onkeydown, page);
}
pub fn setOnKeyPress(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onkeypress, callback, page);
}
pub fn getOnKeyPress(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onkeypress, page);
}
pub fn setOnKeyUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onkeyup, callback, page);
}
pub fn getOnKeyUp(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onkeyup, page);
}
pub fn setOnLoad(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onload, callback, page);
}
pub fn getOnLoad(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onload, page);
}
pub fn setOnLoadedData(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onloadeddata, callback, page);
}
pub fn getOnLoadedData(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onloadeddata, page);
}
pub fn setOnLoadedMetadata(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onloadedmetadata, callback, page);
}
pub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onloadedmetadata, page);
}
pub fn setOnLoadStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onloadstart, callback, page);
}
pub fn getOnLoadStart(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onloadstart, page);
}
pub fn setOnLostPointerCapture(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onlostpointercapture, callback, page);
}
pub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onlostpointercapture, page);
}
pub fn setOnMouseDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onmousedown, callback, page);
}
pub fn getOnMouseDown(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onmousedown, page);
}
pub fn setOnMouseMove(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onmousemove, callback, page);
}
pub fn getOnMouseMove(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onmousemove, page);
}
pub fn setOnMouseOut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onmouseout, callback, page);
}
pub fn getOnMouseOut(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onmouseout, page);
}
pub fn setOnMouseOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onmouseover, callback, page);
}
pub fn getOnMouseOver(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onmouseover, page);
}
pub fn setOnMouseUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onmouseup, callback, page);
}
pub fn getOnMouseUp(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onmouseup, page);
}
pub fn setOnPaste(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpaste, callback, page);
}
pub fn getOnPaste(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpaste, page);
}
pub fn setOnPause(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpause, callback, page);
}
pub fn getOnPause(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpause, page);
}
pub fn setOnPlay(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onplay, callback, page);
}
pub fn getOnPlay(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onplay, page);
}
pub fn setOnPlaying(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onplaying, callback, page);
}
pub fn getOnPlaying(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onplaying, page);
}
pub fn setOnPointerCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointercancel, callback, page);
}
pub fn getOnPointerCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointercancel, page);
}
pub fn setOnPointerDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerdown, callback, page);
}
pub fn getOnPointerDown(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerdown, page);
}
pub fn setOnPointerEnter(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerenter, callback, page);
}
pub fn getOnPointerEnter(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerenter, page);
}
pub fn setOnPointerLeave(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerleave, callback, page);
}
pub fn getOnPointerLeave(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerleave, page);
}
pub fn setOnPointerMove(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointermove, callback, page);
}
pub fn getOnPointerMove(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointermove, page);
}
pub fn setOnPointerOut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerout, callback, page);
}
pub fn getOnPointerOut(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerout, page);
}
pub fn setOnPointerOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerover, callback, page);
}
pub fn getOnPointerOver(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerover, page);
}
pub fn setOnPointerRawUpdate(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerrawupdate, callback, page);
}
pub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerrawupdate, page);
}
pub fn setOnPointerUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onpointerup, callback, page);
}
pub fn getOnPointerUp(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onpointerup, page);
}
pub fn setOnProgress(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onprogress, callback, page);
}
pub fn getOnProgress(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onprogress, page);
}
pub fn setOnRateChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onratechange, callback, page);
}
pub fn getOnRateChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onratechange, page);
}
pub fn setOnReset(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onreset, callback, page);
}
pub fn getOnReset(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onreset, page);
}
pub fn setOnResize(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onresize, callback, page);
}
pub fn getOnResize(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onresize, page);
}
pub fn setOnScroll(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onscroll, callback, page);
}
pub fn getOnScroll(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onscroll, page);
}
pub fn setOnScrollEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onscrollend, callback, page);
}
pub fn getOnScrollEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onscrollend, page);
}
pub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onsecuritypolicyviolation, callback, page);
}
pub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onsecuritypolicyviolation, page);
}
pub fn setOnSeeked(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onseeked, callback, page);
}
pub fn getOnSeeked(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onseeked, page);
}
pub fn setOnSeeking(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onseeking, callback, page);
}
pub fn getOnSeeking(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onseeking, page);
}
pub fn setOnSelect(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onselect, callback, page);
}
pub fn getOnSelect(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onselect, page);
}
pub fn setOnSelectionChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onselectionchange, callback, page);
}
pub fn getOnSelectionChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onselectionchange, page);
}
pub fn setOnSelectStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onselectstart, callback, page);
}
pub fn getOnSelectStart(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onselectstart, page);
}
pub fn setOnSlotChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onslotchange, callback, page);
}
pub fn getOnSlotChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onslotchange, page);
}
pub fn setOnStalled(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onstalled, callback, page);
}
pub fn getOnStalled(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onstalled, page);
}
pub fn setOnSubmit(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onsubmit, callback, page);
}
pub fn getOnSubmit(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onsubmit, page);
}
pub fn setOnSuspend(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onsuspend, callback, page);
}
pub fn getOnSuspend(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onsuspend, page);
}
pub fn setOnTimeUpdate(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontimeupdate, callback, page);
}
pub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontimeupdate, page);
}
pub fn setOnToggle(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontoggle, callback, page);
}
pub fn getOnToggle(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontoggle, page);
}
pub fn setOnTransitionCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontransitioncancel, callback, page);
}
pub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontransitioncancel, page);
}
pub fn setOnTransitionEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontransitionend, callback, page);
}
pub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontransitionend, page);
}
pub fn setOnTransitionRun(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontransitionrun, callback, page);
}
pub fn getOnTransitionRun(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontransitionrun, page);
}
pub fn setOnTransitionStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.ontransitionstart, callback, page);
}
pub fn getOnTransitionStart(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.ontransitionstart, page);
}
pub fn setOnVolumeChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onvolumechange, callback, page);
}
pub fn getOnVolumeChange(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onvolumechange, page);
}
pub fn setOnWaiting(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onwaiting, callback, page);
}
pub fn getOnWaiting(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onwaiting, page);
}
pub fn setOnWheel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {
return self.setAttributeListener(.onwheel, callback, page);
}
pub fn getOnWheel(self: *HtmlElement, page: *Page) !?js.Function.Global {
return self.getAttributeFunction(.onwheel, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HtmlElement);
pub const Meta = struct {
pub const name = "HTMLElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(HtmlElement.construct, .{});
pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{});
fn _innerText(self: *HtmlElement, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });
pub const click = bridge.function(HtmlElement.click, .{});
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});
pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});
pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{});
pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{});
pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{});
pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{});
pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{});
pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{});
pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{});
pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{});
pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{});
pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{});
pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{});
pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{});
pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{});
pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{});
pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{});
pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{});
pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{});
pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{});
pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{});
pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{});
pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{});
pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{});
pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{});
pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{});
pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{});
pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{});
pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{});
pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{});
pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{});
pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{});
pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{});
pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{});
pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{});
pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{});
pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{});
pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{});
pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{});
pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{});
pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{});
pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{});
pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{});
pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{});
pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{});
pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{});
pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{});
pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{});
pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{});
pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{});
pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{});
pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{});
pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{});
pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{});
pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{});
pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{});
pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{});
pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{});
pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{});
pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{});
pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{});
pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{});
pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{});
pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{});
pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{});
pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{});
pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{});
pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{});
pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{});
pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{});
pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{});
pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{});
pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{});
pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{});
pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{});
pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{});
pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{});
pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{});
pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{});
pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{});
pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{});
pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{});
pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{});
pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{});
pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{});
pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{});
pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{});
pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{});
pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{});
pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{});
pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{});
pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{});
pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{});
pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{});
pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{});
};
pub const Build = struct {
// Calls `func_name` with `args` on the most specific type where it is
// implement. This could be on the HtmlElement itself.
pub fn call(self: *const HtmlElement, comptime func_name: []const u8, args: anytype) !bool {
inline for (@typeInfo(HtmlElement.Type).@"union".fields) |f| {
if (@field(HtmlElement.Type, f.name) == self._type) {
// The inner type implements this function. Call it and we're done.
const S = reflect.Struct(f.type);
if (@hasDecl(S, "Build")) {
if (@hasDecl(S.Build, func_name)) {
try @call(.auto, @field(S.Build, func_name), args);
return true;
}
}
}
}
if (@hasDecl(HtmlElement.Build, func_name)) {
// Our last resort - the node implements this function.
try @call(.auto, @field(HtmlElement.Build, func_name), args);
return true;
}
// inform our caller (the Element) that we didn't find anything that implemented
// func_name and it should keep searching for a match.
return false;
}
};
const testing = @import("../../../testing.zig");
test "WebApi: HTML.event_listeners" {
try testing.htmlRunner("element/html/event_listeners.html", .{});
}
test "WebApi: HTMLElement.props" {
try testing.htmlRunner("element/html/htmlelement-props.html", .{});
}
================================================
FILE: src/browser/webapi/element/Svg.zig
================================================
// Copyright (C) 2023-2025 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 String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
pub const Generic = @import("svg/Generic.zig");
const Svg = @This();
_type: Type,
_proto: *Element,
_tag_name: String, // Svg elements are case-preserving
pub const Type = union(enum) {
svg,
generic: *Generic,
};
pub fn is(self: *Svg, comptime T: type) ?*T {
inline for (@typeInfo(Type).@"union".fields) |f| {
if (@field(Type, f.name) == self._type) {
if (f.type == T) {
return &@field(self._type, f.name);
}
if (f.type == *T) {
return @field(self._type, f.name);
}
}
}
return null;
}
pub fn asElement(self: *Svg) *Element {
return self._proto;
}
pub fn asNode(self: *Svg) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Svg);
pub const Meta = struct {
pub const name = "SVGElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
const testing = @import("../../../testing.zig");
test "WebApi: Svg" {
try testing.htmlRunner("element/svg", .{});
}
================================================
FILE: src/browser/webapi/element/html/Anchor.zig
================================================
// Copyright (C) 2023-2025 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 std = @import("std");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Anchor = @This();
_proto: *HtmlElement,
pub fn asElement(self: *Anchor) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Anchor) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Anchor) *Node {
return self.asElement().asNode();
}
pub fn getHref(self: *Anchor, page: *Page) ![]const u8 {
const element = self.asElement();
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return "";
if (href.len == 0) {
return "";
}
return URL.resolve(page.call_arena, page.base(), href, .{ .encode = true });
}
pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("href"), .wrap(value), page);
}
pub fn getTarget(self: *Anchor) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("target")) orelse "";
}
pub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), page);
}
pub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return (try URL.getOrigin(page.call_arena, href)) orelse "null";
}
pub fn getHost(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
const host = URL.getHost(href);
const protocol = URL.getProtocol(href);
const port = URL.getPort(href);
// Strip default ports
if (port.len > 0) {
if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or
(std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80")))
{
return URL.getHostname(href);
}
}
return host;
}
pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setHost(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return URL.getHostname(href);
}
pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setHostname(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getPort(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
const port = URL.getPort(href);
const protocol = URL.getProtocol(href);
// Return empty string for default ports
if (port.len > 0) {
if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or
(std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80")))
{
return "";
}
}
return port;
}
pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setPort(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return URL.getSearch(href);
}
pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setSearch(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getHash(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return URL.getHash(href);
}
pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setHash(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getPathname(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return URL.getPathname(href);
}
pub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setPathname(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 {
const href = try getResolvedHref(self, page) orelse return "";
return URL.getProtocol(href);
}
pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void {
const href = try getResolvedHref(self, page) orelse return;
const new_href = try URL.setProtocol(href, value, page.call_arena);
try setHref(self, new_href, page);
}
pub fn getType(self: *Anchor) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("type")) orelse "";
}
pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page);
}
pub fn getName(self: *const Anchor) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page);
}
pub fn getText(self: *Anchor, page: *Page) ![:0]const u8 {
return self.asNode().getTextContentAlloc(page.call_arena);
}
pub fn setText(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asNode().setTextContent(value, page);
}
fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 {
const href = self.asElement().getAttributeSafe(comptime .wrap("href")) orelse return null;
if (href.len == 0) {
return null;
}
return try URL.resolve(page.call_arena, page.base(), href, .{});
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Anchor);
pub const Meta = struct {
pub const name = "HTMLAnchorElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{});
pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{});
pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{});
pub const origin = bridge.accessor(Anchor.getOrigin, null, .{});
pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{});
pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{});
pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{});
pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{});
pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{});
pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{});
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
pub const toString = bridge.function(Anchor.getHref, .{});
fn _getRelList(self: *Anchor, page: *Page) !?*@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(page);
}
};
const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Anchor" {
try testing.htmlRunner("element/html/anchor.html", .{});
}
================================================
FILE: src/browser/webapi/element/html/Area.zig
================================================
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Area = @This();
_proto: *HtmlElement,
pub fn asElement(self: *Area) *Element {
return self._proto._proto;
}
pub fn asNode(self: *Area) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Area);
pub const Meta = struct {
pub const name = "HTMLAreaElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
================================================
FILE: src/browser/webapi/element/html/Audio.zig
================================================
// Copyright (C) 2023-2025 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 String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const Media = @import("Media.zig");
const Audio = @This();
_proto: *Media,
pub fn constructor(maybe_url: ?String, page: *Page) !*Media {
const node = try page.createElementNS(.html, "audio", null);
const el = node.as(Element);
const list = try el.getOrCreateAttributeList(page);
// Always set to "auto" initially.
_ = try list.putSafe(comptime .wrap("preload"), comptime .wrap("auto"), el, page);
// Set URL if provided.
if (maybe_url) |url| {
_ = try list.putSafe(comptime .wrap("src"), url, el, page);
}
return node.as(Media);
}
pub fn asMedia(self: *Audio) *Media {
return self._proto;
}
pub fn asElement(self: *Audio) *Element {
return self._proto.asElement();
}
pub fn asNode(self: *Audio) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Audio);
pub const Meta = struct {
pub const name = "HTMLAudioElement";
pub const constructor_alias = "Audio";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(Audio.constructor, .{});
};
================================================
FILE: src/browser/webapi/element/html/BR.zig
================================================
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const BR = @This();
_proto: *HtmlElement,
pub fn asElement(self: *BR) *Element {
return self._proto._proto;
}
pub fn asNode(self: *BR) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(BR);
pub const Meta = struct {
pub const name = "HTMLBRElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
================================================
FILE: src/browser/webapi/element/html/Base.zig
================================================
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Base = @This();
_proto: *HtmlElement,
pub fn asElement(self: *Base) *Element {
return self._proto._proto;
}
pub fn asNode(self: *Base) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Base);
pub const Meta = struct {
pub const name = "HTMLBaseElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};
================================================
FILE: src/browser/webapi/element/html/Body.zig
================================================
// Copyright (C) 2023-2025 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 log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Body = @This();
_proto: *HtmlElement,
pub fn asElement(self: *Body) *Element {
return self._proto._proto;
}
pub fn asNode(self: *Body) *Node {
return self.asElement().asNode();
}
/// Special-case: `body.onload` is actually an alias for `window.onload`.
pub fn setOnLoad(_: *Body, callback: ?js.Function.Global, page: *Page) !void {
page.window._on_load = callback;
}
/// Special-case: `body.onload` is actually an alias for `window.onload`.
pub fn getOnLoad(_: *Body, page: *Page) ?js.Function.Global {
return page.window._on_load;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Body);
pub const Meta = struct {
pub const name = "HTMLBodyElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const onload = bridge.accessor(getOnLoad, setOnLoad, .{ .null_as_undefined = false });
};
pub const Build = struct {
pub fn complete(node: *Node, page: *Page) !void {
const el = node.as(Element);
const on_load = el.getAttributeSafe(comptime .wrap("onload")) orelse return;
if (page.js.stringToPersistedFunction(on_load, &.{"event"}, &.{})) |func| {
page.window._on_load = func;
} else |err| {
log.err(.js, "body.onload", .{ .err = err, .str = on_load });
}
}
};
================================================
FILE: src/browser/webapi/element/html/Button.zig
================================================
// Copyright (C) 2023-2025 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 Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Form = @import("Form.zig");
const Button = @This();
_proto: *HtmlElement,
pub fn asElement(self: *Button) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Button) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Button) *Node {
return self.asElement().asNode();
}
pub fn getDisabled(self: *const Button) bool {
return self.asConstElement().getAttributeSafe(comptime .wrap("disabled")) != null;
}
pub fn setDisabled(self: *Button, disabled: bool, page: *Page) !void {
if (disabled) {
try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page);
} else {
try self.asElement().removeAttribute(comptime .wrap("disabled"), page);
}
}
pub fn getName(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Button, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub fn getType(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "submit";
}
pub fn setType(self: *Button, typ: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(typ), page);
}
pub fn getValue(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("value")) orelse "";
}
pub fn setValue(self: *Button, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(value), page);
}
pub fn getRequired(self: *const Button) bool {
return self.asConstElement().getAttributeSafe(comptime .wrap("required")) != null;
}
pub fn setRequired(self: *Button, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe(comptime .wrap("required"), .wrap(""), page);
} else {
try self.asElement().removeAttribute(comptime .wrap("required"), page);
}
}
pub fn getForm(self: *Button, page: *Page) ?*Form {
const element = self.asElement();
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| {
return form_element.is(Form);
}
// form attribute present but invalid - no form owner
return null;
}
// No form attribute - traverse ancestors looking for a