Files
browser/src/browser/webapi/Element.zig
Karl Seguin e3e3fc25fe Various HTML attribute tweaks
Aimed at improving WPT /html/dom/reflection-obsolete.html Goes from 923 to 2305
passing cases (the remaining failing cases are all for <frame> which we don't
currently support)

Add accessors to Directory, Font and FrameSet. Add HTMLMarqueeElement.

Font setColor null -> ""

Add new properties to Html (accessKey and autofocus) and improve tabIndex
parsing.
2026-06-02 17:33:37 +08:00

1914 lines
65 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Frame = @import("../Frame.zig");
const StyleManager = @import("../StyleManager.zig");
const reflect = @import("../reflect.zig");
const Node = @import("Node.zig");
const CSS = @import("CSS.zig");
const ShadowRoot = @import("ShadowRoot.zig");
const EventTarget = @import("EventTarget.zig");
const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig");
const Animation = @import("animation/Animation.zig");
const DOMStringMap = @import("element/DOMStringMap.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMRect = @import("DOMRect.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
pub const Attribute = @import("element/Attribute.zig");
const log = lp.log;
const String = lp.String;
const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);
pub const ScrollPosition = struct {
x: u32 = 0,
y: u32 = 0,
};
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
pub const Namespace = enum(u8) {
html,
svg,
mathml,
xml,
// We should keep the original value, but don't. If this becomes important
// consider storing it in a frame lookup, like `_element_class_lists`, rather
// that adding a slice directly here (directly in every element).
unknown,
null,
pub fn toUri(self: Namespace) ?[]const u8 {
return switch (self) {
.html => "http://www.w3.org/1999/xhtml",
.svg => "http://www.w3.org/2000/svg",
.mathml => "http://www.w3.org/1998/Math/MathML",
.xml => "http://www.w3.org/XML/1998/namespace",
.unknown => "http://lightpanda.io/unsupported/namespace",
.null => null,
};
}
pub fn parse(namespace_: ?[]const u8) Namespace {
const namespace = namespace_ orelse return .null;
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
// Common case, avoid the string comparison. Recklessly
@branchHint(.likely);
return .html;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
return .xml;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/svg")) {
return .svg;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/1998/Math/MathML")) {
return .mathml;
}
return .unknown;
}
};
_type: Type,
_proto: *Node,
_namespace: Namespace = .html,
_attributes: ?*Attribute.List = null,
pub const Type = union(enum) {
html: *Html,
svg: *Svg,
};
pub fn is(self: *Element, comptime T: type) ?*T {
const type_name = @typeName(T);
switch (self._type) {
.html => |el| {
if (T == Html) {
return el;
}
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
return el.is(T);
}
},
.svg => |svg| {
if (T == Svg) {
return svg;
}
if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
return svg.is(T);
}
},
}
return null;
}
pub fn as(self: *Element, comptime T: type) *T {
return self.is(T).?;
}
pub fn asNode(self: *Element) *Node {
return self._proto;
}
pub fn asEventTarget(self: *Element) *EventTarget {
return self._proto.asEventTarget();
}
pub fn asConstNode(self: *const Element) *const Node {
return self._proto;
}
pub fn attributesEql(self: *const Element, other: *Element) bool {
if (self._attributes) |attr_list| {
const other_list = other._attributes orelse return false;
return attr_list.eql(other_list);
}
// Make sure no attrs in both sides.
return other._attributes == null;
}
/// TODO: localName and prefix comparison.
pub fn isEqualNode(self: *Element, other: *Element) bool {
const self_tag = self.getTagNameDump();
const other_tag = other.getTagNameDump();
// Compare namespaces and tags.
const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag);
if (dirty) {
return false;
}
// Compare attributes.
if (!self.attributesEql(other)) {
return false;
}
// Compare children.
var self_iter = self.asNode().childrenIterator();
var other_iter = other.asNode().childrenIterator();
var self_count: usize = 0;
var other_count: usize = 0;
while (self_iter.next()) |self_node| : (self_count += 1) {
const other_node = other_iter.next() orelse return false;
other_count += 1;
if (self_node.isEqualNode(other_node)) {
continue;
}
return false;
}
// Make sure both have equal number of children.
return self_count == other_count;
}
pub fn getTagNameLower(self: *const Element) []const u8 {
switch (self._type) {
.html => |he| switch (he._type) {
.custom => |ce| {
@branchHint(.unlikely);
return ce._tag_name.str();
},
else => return switch (he._type) {
.anchor => "a",
.area => "area",
.base => "base",
.body => "body",
.br => "br",
.button => "button",
.canvas => "canvas",
.custom => |e| e._tag_name.str(),
.data => "data",
.datalist => "datalist",
.details => "details",
.dialog => "dialog",
.directory => "dir",
.div => "div",
.dl => "dl",
.embed => "embed",
.fieldset => "fieldset",
.font => "font",
.frameset => "frameset",
.form => "form",
.generic => |e| e._tag_name.str(),
.heading => |e| e._tag_name.str(),
.head => "head",
.html => "html",
.hr => "hr",
.iframe => "iframe",
.img => "img",
.input => "input",
.label => "label",
.legend => "legend",
.li => "li",
.link => "link",
.map => "map",
.marquee => "marquee",
.media => |m| switch (m._type) {
.audio => "audio",
.video => "video",
.generic => "media",
},
.meta => "meta",
.meter => "meter",
.mod => |e| e._tag_name.str(),
.object => "object",
.ol => "ol",
.optgroup => "optgroup",
.option => "option",
.output => "output",
.p => "p",
.picture => "picture",
.param => "param",
.pre => "pre",
.progress => "progress",
.quote => |e| e._tag_name.str(),
.script => "script",
.select => "select",
.slot => "slot",
.source => "source",
.span => "span",
.style => "style",
.table => "table",
.table_caption => "caption",
.table_cell => |e| e._tag_name.str(),
.table_col => |e| e._tag_name.str(),
.table_row => "tr",
.table_section => |e| e._tag_name.str(),
.template => "template",
.textarea => "textarea",
.time => "time",
.title => "title",
.track => "track",
.ul => "ul",
.unknown => |e| e._tag_name.str(),
},
},
.svg => |svg| return svg._tag_name.str(),
}
}
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
return switch (self._type) {
.html => |he| switch (he._type) {
.anchor => "A",
.area => "AREA",
.base => "BASE",
.body => "BODY",
.br => "BR",
.button => "BUTTON",
.canvas => "CANVAS",
.custom => |e| upperTagName(&e._tag_name, buf),
.data => "DATA",
.datalist => "DATALIST",
.details => "DETAILS",
.dialog => "DIALOG",
.directory => "DIR",
.div => "DIV",
.dl => "DL",
.embed => "EMBED",
.fieldset => "FIELDSET",
.font => "FONT",
.frameset => "FRAMESET",
.form => "FORM",
.generic => |e| upperTagName(&e._tag_name, buf),
.heading => |e| upperTagName(&e._tag_name, buf),
.head => "HEAD",
.html => "HTML",
.hr => "HR",
.iframe => "IFRAME",
.img => "IMG",
.input => "INPUT",
.label => "LABEL",
.legend => "LEGEND",
.li => "LI",
.link => "LINK",
.map => "MAP",
.marquee => "MARQUEE",
.meta => "META",
.media => |m| switch (m._type) {
.audio => "AUDIO",
.video => "VIDEO",
.generic => "MEDIA",
},
.meter => "METER",
.mod => |e| upperTagName(&e._tag_name, buf),
.object => "OBJECT",
.ol => "OL",
.optgroup => "OPTGROUP",
.option => "OPTION",
.output => "OUTPUT",
.p => "P",
.picture => "PICTURE",
.param => "PARAM",
.pre => "PRE",
.progress => "PROGRESS",
.quote => |e| upperTagName(&e._tag_name, buf),
.script => "SCRIPT",
.select => "SELECT",
.slot => "SLOT",
.source => "SOURCE",
.span => "SPAN",
.style => "STYLE",
.table => "TABLE",
.table_caption => "CAPTION",
.table_cell => |e| upperTagName(&e._tag_name, buf),
.table_col => |e| upperTagName(&e._tag_name, buf),
.table_row => "TR",
.table_section => |e| upperTagName(&e._tag_name, buf),
.template => "TEMPLATE",
.textarea => "TEXTAREA",
.time => "TIME",
.title => "TITLE",
.track => "TRACK",
.ul => "UL",
.unknown => |e| switch (self._namespace) {
.html => upperTagName(&e._tag_name, buf),
.svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
},
},
.svg => |svg| svg._tag_name.str(),
};
}
pub fn getTagNameDump(self: *const Element) []const u8 {
switch (self._type) {
.html => return self.getTagNameLower(),
.svg => |svg| return svg._tag_name.str(),
}
}
pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
return self._namespace.toUri();
}
pub fn getNamespaceUri(self: *Element, frame: *Frame) ?[]const u8 {
if (self._namespace != .unknown) return self._namespace.toUri();
return frame._element_namespace_uris.get(self);
}
pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, frame: *Frame) ?[]const u8 {
// Hardcoded reserved prefixes
if (prefix) |p| {
if (std.mem.eql(u8, p, "xml")) return "http://www.w3.org/XML/1998/namespace";
if (std.mem.eql(u8, p, "xmlns")) return "http://www.w3.org/2000/xmlns/";
}
// Step 1: check element's own namespace/prefix
if (self.getNamespaceUri(frame)) |ns_uri| {
const el_prefix = self._prefix();
const match = if (prefix == null and el_prefix == null)
true
else if (prefix != null and el_prefix != null)
std.mem.eql(u8, prefix.?, el_prefix.?)
else
false;
if (match) return ns_uri;
}
// Step 2: search xmlns attributes
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
if (prefix == null) {
if (entry._name.eql(comptime .wrap("xmlns"))) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
} else {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:")) {
if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
}
}
}
}
// Step 3: recurse to parent element
const parent = self.asNode().parentElement() orelse return null;
return parent.lookupNamespaceURIForElement(prefix, frame);
}
fn _prefix(self: *const Element) ?[]const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[0..pos];
}
return null;
}
pub fn getLocalName(self: *Element) []const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[pos + 1 ..];
}
return name;
}
// Wrapper methods that delegate to Html implementations
pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.getInnerText(writer);
}
pub fn setInnerText(self: *Element, text: []const u8, frame: *Frame) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.setInnerText(text, frame);
}
pub fn insertAdjacentHTML(
self: *Element,
position: []const u8,
html_or_xml: []const u8,
frame: *Frame,
) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.insertAdjacentHTML(position, html_or_xml, frame);
}
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
const dump = @import("../dump.zig");
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, frame);
}
pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
const node = self.asNode();
const parent = node._parent orelse return;
frame.domChanged();
if (html.len > 0) {
const fragment = (try Node.DocumentFragment.init(frame)).asNode();
try frame.parseHtmlAsChildren(fragment, html);
try frame.insertAllChildrenBefore(fragment, parent, node);
}
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, frame);
}
pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, false, frame);
}
/// allows declarative shadow dom
pub fn setHTMLUnsafe(self: *Element, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
return parent.setHTML(html, true, frame);
}
pub fn getId(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("id")) orelse "";
}
pub fn setId(self: *Element, value: []const u8, frame: *Frame) !void {
return self.setAttributeSafe(comptime .wrap("id"), .wrap(value), frame);
}
pub fn getSlot(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("slot")) orelse "";
}
pub fn setSlot(self: *Element, value: []const u8, frame: *Frame) !void {
return self.setAttributeSafe(comptime .wrap("slot"), .wrap(value), frame);
}
pub fn getDir(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("dir")) orelse "";
}
pub fn setDir(self: *Element, value: []const u8, frame: *Frame) !void {
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), frame);
}
// ARIAMixin - ARIA attribute reflection
pub fn getAriaAtomic(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-atomic"));
}
pub fn setAriaAtomic(self: *Element, value: ?[]const u8, frame: *Frame) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-atomic"), .wrap(v), frame);
} else {
try self.removeAttribute(comptime .wrap("aria-atomic"), frame);
}
}
pub fn getAriaLive(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-live"));
}
pub fn setAriaLive(self: *Element, value: ?[]const u8, frame: *Frame) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-live"), .wrap(v), frame);
} else {
try self.removeAttribute(comptime .wrap("aria-live"), frame);
}
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}
pub fn setClassName(self: *Element, value: []const u8, frame: *Frame) !void {
return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), frame);
}
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
const attributes = self._attributes orelse return .{};
return attributes.iterator();
}
pub fn getAttribute(self: *const Element, name: String, frame: *Frame) !?String {
const attributes = self._attributes orelse return null;
return attributes.get(name, frame);
}
/// For simplicity, the namespace is currently ignored and only the local name is used.
pub fn getAttributeNS(
self: *const Element,
maybe_namespace: ?[]const u8,
local_name: String,
frame: *Frame,
) !?String {
if (maybe_namespace) |namespace| {
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
}
}
return self.getAttribute(local_name, frame);
}
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
}
pub fn hasAttribute(self: *const Element, name: String, frame: *Frame) !bool {
const attributes = self._attributes orelse return false;
const value = try attributes.get(name, frame);
return value != null;
}
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
const attributes = self._attributes orelse return false;
return attributes.hasSafe(name);
}
// Per HTML "concept-fe-disabled", only listed elements participate in the
// disabled concept. Anything else (e.g. <div disabled>) has no disabled
// state and never matches :disabled / :enabled.
pub fn hasDisabledConcept(self: *const Element) bool {
return switch (self.getTag()) {
.button, .input, .select, .textarea, .optgroup, .option, .fieldset => true,
else => false,
};
}
pub fn isDisabled(self: *Element) bool {
if (!self.hasDisabledConcept()) {
return false;
}
if (self.getAttributeSafe(comptime .wrap("disabled")) != null) {
return true;
}
// <option> takes a different inheritance path: per HTML
// "concept-option-disabled" an option is disabled when its parent is an
// <optgroup disabled>. It does NOT inherit from <select disabled> or
// an ancestor <fieldset disabled>.
if (self.getTag() == .option) {
if (self.asNode()._parent) |parent_node| {
if (parent_node.is(Element)) |parent_el| {
if (parent_el.getTag() == .optgroup and
parent_el.getAttributeSafe(comptime .wrap("disabled")) != null)
{
return true;
}
}
}
return false;
}
const element_node = self.asNode();
var current: ?*Node = element_node._parent;
while (current) |node| {
current = node._parent;
const ancestor = node.is(Element) orelse continue;
if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) {
var child = ancestor.firstElementChild();
while (child) |c| {
if (c.getTag() == .legend) {
if (c.asNode().contains(element_node)) return false;
break;
}
child = c.nextElementSibling();
}
return true;
}
}
return false;
}
pub fn hasAttributes(self: *const Element) bool {
const attributes = self._attributes orelse return false;
return attributes.isEmpty() == false;
}
pub fn getAttributeNode(self: *Element, name: String, frame: *Frame) !?*Attribute {
const attributes = self._attributes orelse return null;
return attributes.getAttribute(name, self, frame);
}
pub fn setAttribute(self: *Element, name: String, value: String, frame: *Frame) !void {
try Attribute.validateAttributeName(name);
const attributes = try self.getOrCreateAttributeList(frame);
_ = try attributes.put(name, value, self, frame);
}
pub fn setAttributeNS(
self: *Element,
maybe_namespace: ?[]const u8,
qualified_name: []const u8,
value: String,
frame: *Frame,
) !void {
const attr_name = if (maybe_namespace) |namespace| blk: {
// For xmlns namespace, store the full qualified name (e.g. "xmlns:bar")
// so lookupNamespaceURI can find namespace declarations.
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
break :blk qualified_name;
}
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
}
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
} else blk: {
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
};
return self.setAttribute(.wrap(attr_name), value, frame);
}
pub fn setAttributeSafe(self: *Element, name: String, value: String, frame: *Frame) !void {
const attributes = try self.getOrCreateAttributeList(frame);
_ = try attributes.putSafe(name, value, self, frame);
}
pub fn getOrCreateAttributeList(self: *Element, frame: *Frame) !*Attribute.List {
return self._attributes orelse return self.createAttributeList(frame);
}
pub fn createAttributeList(self: *Element, frame: *Frame) !*Attribute.List {
lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
const a = try frame.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html };
self._attributes = a;
return a;
}
pub fn getShadowRoot(self: *Element, frame: *Frame) ?*ShadowRoot {
const shadow_root = frame._element_shadow_roots.get(self) orelse return null;
if (shadow_root._mode == .closed) return null;
return shadow_root;
}
pub fn getAssignedSlot(self: *Element, frame: *Frame) ?*Html.Slot {
return frame._element_assigned_slots.get(self);
}
// Whether this element may host a shadow root
fn isValidShadowHost(self: *const Element) bool {
if (self._namespace != .html) {
return false;
}
return switch (self.getTag()) {
.article, .aside, .blockquote, .body, .div, .footer, .header, .main, .nav, .p, .section, .span, .h1, .h2, .h3, .h4, .h5, .h6, .custom => true,
else => false,
};
}
pub fn attachShadow(self: *Element, mode_str: String, frame: *Frame) !*ShadowRoot {
if (frame._element_shadow_roots.get(self)) |_| {
return error.NotSupported;
}
if (!self.isValidShadowHost()) {
return error.NotSupported;
}
// A custom element whose definition lists "shadow" in disabledFeatures
// cannot host a shadow root (imperative or declarative).
if (self.is(Html.Custom)) |custom| {
if (frame.window._custom_elements._definitions.get(custom._tag_name.str())) |def| {
if (def.disable_shadow) {
return error.NotSupported;
}
}
}
const mode: ShadowRoot.Mode = blk: {
if (mode_str.eql(comptime .wrap("open"))) break :blk .open;
if (mode_str.eql(comptime .wrap("closed"))) break :blk .closed;
return error.InvalidArgument;
};
const shadow_root = try ShadowRoot.init(self, mode, frame);
try frame._element_shadow_roots.put(frame.arena, self, shadow_root);
return shadow_root;
}
pub fn insertAdjacentElement(
self: *Element,
position: []const u8,
element: *Element,
frame: *Frame,
) !void {
const target_node, const prev_node = try self.asNode().findAdjacentNodes(position);
_ = try target_node.insertBefore(element.asNode(), prev_node, frame);
}
pub fn insertAdjacentText(
self: *Element,
where: []const u8,
data: []const u8,
frame: *Frame,
) !void {
const text_node = try frame.createTextNode(data);
const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);
_ = try target_node.insertBefore(text_node, prev_node, frame);
}
pub fn setAttributeNode(self: *Element, attr: *Attribute, frame: *Frame) !?*Attribute {
if (attr._element) |el| {
if (el == self) {
return attr;
}
attr._element = null;
_ = try el.removeAttributeNode(attr, frame);
}
const attributes = try self.getOrCreateAttributeList(frame);
return attributes.putAttribute(attr, self, frame);
}
pub fn removeAttribute(self: *Element, name: String, frame: *Frame) !void {
const attributes = self._attributes orelse return;
return attributes.delete(name, self, frame);
}
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, frame: *Frame) !bool {
try Attribute.validateAttributeName(name);
const has = try self.hasAttribute(name, frame);
const should_add = force orelse !has;
if (should_add and !has) {
try self.setAttribute(name, String.empty, frame);
return true;
} else if (!should_add and has) {
try self.removeAttribute(name, frame);
return false;
}
return should_add;
}
pub fn removeAttributeNode(self: *Element, attr: *Attribute, frame: *Frame) !*Attribute {
if (attr._element == null or attr._element.? != self) {
return error.NotFound;
}
try self.removeAttribute(attr._name, frame);
attr._element = null;
return attr;
}
pub fn getAttributeNames(self: *const Element, frame: *Frame) ![][]const u8 {
const attributes = self._attributes orelse return &.{};
return attributes.getNames(frame);
}
pub fn getAttributeNamedNodeMap(self: *Element, frame: *Frame) !*Attribute.NamedNodeMap {
const gop = try frame._attribute_named_node_map_lookup.getOrPut(frame.arena, @intFromPtr(self));
if (!gop.found_existing) {
const attributes = try self.getOrCreateAttributeList(frame);
const named_node_map = try frame._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
gop.value_ptr.* = named_node_map;
}
return gop.value_ptr.*;
}
pub fn getOrCreateStyle(self: *Element, frame: *Frame) !*CSSStyleProperties {
const gop = try frame._element_styles.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try CSSStyleProperties.init(self, false, frame);
}
return gop.value_ptr.*;
}
fn getStyle(self: *Element, frame: *Frame) ?*CSSStyleProperties {
return frame._element_styles.get(self);
}
pub fn getClassList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_class_lists.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = comptime .wrap("class"),
});
}
return gop.value_ptr.*;
}
pub fn setClassList(self: *Element, value: String, frame: *Frame) !void {
const class_list = try self.getClassList(frame);
try class_list.setValue(value, frame);
}
pub fn getRelList(self: *Element, frame: *Frame) !*collections.DOMTokenList {
const gop = try frame._element_rel_lists.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = comptime .wrap("rel"),
});
}
return gop.value_ptr.*;
}
pub fn getDataset(self: *Element, frame: *Frame) !*DOMStringMap {
const gop = try frame._element_datasets.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try frame._factory.create(DOMStringMap{
._element = self,
});
}
return gop.value_ptr.*;
}
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
return self.asNode().replaceChildren(nodes, frame);
}
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const ref_node = self.asNode();
const parent = ref_node._parent orelse return;
frame.domChanged();
const parent_is_connected = parent.isConnected();
// Detect if the ref_node must be removed (by default) or kept.
// We kept it when ref_node is present into the nodes list.
var rm_ref_node = true;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
// If a child is the ref node. We keep it at its own current position.
if (child == ref_node) {
rm_ref_node = false;
continue;
}
if (child._parent) |current_parent| {
frame.removeNode(current_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try frame.insertNodeRelative(
parent,
child,
.{ .before = ref_node },
.{ .child_already_connected = child.isConnected() },
);
}
// Re-check parent after insertNodeRelative since callbacks (e.g. connectedCallback)
// could have already removed ref_node from parent.
if (rm_ref_node and ref_node._parent == parent) {
frame.removeNode(parent, ref_node, .{ .will_be_reconnected = false });
}
}
pub fn remove(self: *Element, frame: *Frame) void {
const node = self.asNode();
const parent = node._parent orelse return;
frame.domChanged();
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn focus(self: *Element, frame: *Frame) !void {
if (self.asNode().isConnected() == false) {
// a disconnected node cannot take focus
return;
}
const FocusEvent = @import("event/FocusEvent.zig");
const new_target = self.asEventTarget();
const old_active = frame.document._active_element;
frame.document._active_element = self;
if (old_active) |old| {
if (old == self) {
return;
}
const old_target = old.asEventTarget();
// Dispatch blur on old element (no bubble, composed)
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, frame);
try frame._event_manager.dispatch(old_target, blur_event.asEvent());
// Dispatch focusout on old element (bubbles, composed)
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, frame);
try frame._event_manager.dispatch(old_target, focusout_event.asEvent());
}
const old_related: ?*EventTarget = if (old_active) |old| old.asEventTarget() else null;
// Dispatch focus on new element (no bubble, composed)
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, frame);
try frame._event_manager.dispatch(new_target, focus_event.asEvent());
// Dispatch focusin on new element (bubbles, composed)
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, frame);
try frame._event_manager.dispatch(new_target, focusin_event.asEvent());
}
pub fn blur(self: *Element, frame: *Frame) !void {
if (frame.document._active_element != self) return;
frame.document._active_element = null;
const FocusEvent = @import("event/FocusEvent.zig");
const old_target = self.asEventTarget();
// Dispatch blur (no bubble, composed)
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, frame);
try frame._event_manager.dispatch(old_target, blur_event.asEvent());
// Dispatch focusout (bubbles, composed)
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, frame);
try frame._event_manager.dispatch(old_target, focusout_event.asEvent());
}
pub fn getChildren(self: *Element, frame: *Frame) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, frame);
}
pub fn append(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const parent = self.asNode();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
_ = try parent.appendChild(child, frame);
}
}
pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const parent = self.asNode();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(frame);
_ = try parent.insertBefore(child, parent.firstChild(), frame);
}
}
pub fn before(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
_ = try parent.insertBefore(child, node, frame);
}
}
pub fn after(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(frame);
_ = try parent.insertBefore(child, viable_next, frame);
}
}
pub fn firstElementChild(self: *Element) ?*Element {
var maybe_child = self.asNode().firstChild();
while (maybe_child) |child| {
if (child.is(Element)) |el| return el;
maybe_child = child.nextSibling();
}
return null;
}
pub fn lastElementChild(self: *Element) ?*Element {
var maybe_child = self.asNode().lastChild();
while (maybe_child) |child| {
if (child.is(Element)) |el| return el;
maybe_child = child.previousSibling();
}
return null;
}
pub fn nextElementSibling(self: *Element) ?*Element {
var maybe_sibling = self.asNode().nextSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Element)) |el| return el;
maybe_sibling = sibling.nextSibling();
}
return null;
}
pub fn previousElementSibling(self: *Element) ?*Element {
var maybe_sibling = self.asNode().previousSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Element)) |el| return el;
maybe_sibling = sibling.previousSibling();
}
return null;
}
pub fn getChildElementCount(self: *Element) usize {
var count: usize = 0;
var it = self.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element) != null) {
count += 1;
}
}
return count;
}
pub fn matches(self: *Element, selector: []const u8, frame: *Frame) !bool {
return Selector.matches(self, selector, frame) catch |err| Selector.mapErrorToDOM(err);
}
pub fn querySelector(self: *Element, selector: []const u8, frame: *Frame) !?*Element {
return Selector.querySelector(self.asNode(), selector, frame) catch |err| Selector.mapErrorToDOM(err);
}
pub fn querySelectorAll(self: *Element, input: []const u8, frame: *Frame) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input, frame) catch |err| Selector.mapErrorToDOM(err);
}
pub fn getAnimations(_: *const Element) []*Animation {
return &.{};
}
pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, frame: *Frame) !*Animation {
return Animation.init(frame);
}
pub fn closest(self: *Element, selector: []const u8, frame: *Frame) !?*Element {
if (selector.len == 0) {
return error.SyntaxError;
}
var current: ?*Element = self;
while (current) |el| {
if (try Selector.matchesWithScope(el, selector, self, frame)) {
return el;
}
const parent = el._proto._parent orelse break;
if (parent.is(ShadowRoot) != null) {
break;
}
current = parent.is(Element);
}
return null;
}
pub fn parentElement(self: *Element) ?*Element {
return self._proto.parentElement();
}
/// Cache for visibility checks - re-exported from StyleManager for convenience.
pub const VisibilityCache = StyleManager.VisibilityCache;
/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
pub const PointerEventsCache = StyleManager.PointerEventsCache;
pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, frame: *Frame) bool {
return frame._style_manager.hasPointerEventsNone(self, cache);
}
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, frame: *Frame) bool {
return !frame._style_manager.isHidden(self, cache, .{});
}
const CheckVisibilityOpts = struct {
checkOpacity: bool = false,
opacityProperty: bool = false,
checkVisibilityCSS: bool = false,
visibilityProperty: bool = false,
};
pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, frame: *Frame) bool {
const opts = opts_ orelse CheckVisibilityOpts{};
return !frame._style_manager.isHidden(self, null, .{
.check_opacity = opts.checkOpacity or opts.opacityProperty,
.check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
});
}
pub fn getElementDimensions(self: *Element, frame: *Frame) struct { width: f64, height: f64 } {
var width: f64 = 5.0;
var height: f64 = 5.0;
if (self.getStyle(frame)) |style| {
const decl = style.asCSSStyleDeclaration();
width = CSS.parseDimension(decl.getPropertyValue("width", frame)) orelse 5.0;
height = CSS.parseDimension(decl.getPropertyValue("height", frame)) orelse 5.0;
}
if (width == 5.0 or height == 5.0) {
const tag = self.getTag();
// Root containers get large default size to contain descendant positions.
// With calculateDocumentPosition using linear depth scaling (100px per level),
// even very deep trees (100 levels) stay within 10,000px.
// 100M pixels is plausible for very long documents.
if (tag == .html or tag == .body) {
if (width == 5.0) width = 1920.0;
if (height == 5.0) height = 100_000_000.0;
} else if (tag == .img or tag == .iframe) {
if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
width = std.fmt.parseFloat(f64, w) catch width;
}
if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
height = std.fmt.parseFloat(f64, h) catch height;
}
}
}
return .{ .width = width, .height = height };
}
pub fn getClientWidth(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
const dims = self.getElementDimensions(frame);
return dims.width;
}
pub fn getClientHeight(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
const dims = self.getElementDimensions(frame);
return dims.height;
}
pub fn getBoundingClientRect(self: *Element, frame: *Frame) DOMRect {
if (!self.checkVisibilityCached(null, frame)) {
return .{
._x = 0.0,
._y = 0.0,
._width = 0.0,
._height = 0.0,
};
}
return self.getBoundingClientRectForVisible(frame);
}
// Some cases need a the BoundingClientRect but have already done the
// visibility check.
pub fn getBoundingClientRectForVisible(self: *Element, frame: *Frame) DOMRect {
const y = calculateDocumentPosition(self.asNode());
const dims = self.getElementDimensions(frame);
// Use sibling position for x coordinate to ensure siblings have different x values
const x = calculateSiblingPosition(self.asNode());
return .{
._x = x,
._y = y,
._width = dims.width,
._height = dims.height,
};
}
pub fn getClientRects(self: *Element, frame: *Frame) ![]DOMRect {
if (!self.checkVisibilityCached(null, frame)) {
return &.{};
}
const rects = try frame.call_arena.alloc(DOMRect, 1);
rects[0] = self.getBoundingClientRectForVisible(frame);
return rects;
}
pub fn getScrollTop(self: *Element, frame: *Frame) u32 {
const pos = frame._element_scroll_positions.get(self) orelse return 0;
return pos.y;
}
pub fn setScrollTop(self: *Element, value: i32, frame: *Frame) !void {
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.y = @intCast(@max(0, value));
}
pub fn getScrollLeft(self: *Element, frame: *Frame) u32 {
const pos = frame._element_scroll_positions.get(self) orelse return 0;
return pos.x;
}
pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void {
const gop = try frame._element_scroll_positions.getOrPut(frame.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.x = @intCast(@max(0, value));
}
pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientHeight(frame);
}
pub fn getScrollWidth(self: *Element, frame: *Frame) f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientWidth(frame);
}
pub fn getOffsetHeight(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
const dims = self.getElementDimensions(frame);
return dims.height;
}
pub fn getOffsetWidth(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
const dims = self.getElementDimensions(frame);
return dims.width;
}
pub fn getOffsetTop(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
return calculateDocumentPosition(self.asNode());
}
pub fn getOffsetLeft(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
return 0.0;
}
return calculateSiblingPosition(self.asNode());
}
pub fn getClientTop(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
pub fn getClientLeft(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
// Calculates document position by counting all nodes that appear before this one
// in tree order, but only traversing the "left side" of the tree.
//
// This walks up from the target node to the root, and at each level counts:
// 1. All previous siblings and their descendants
// 2. The parent itself
//
// Example:
// <body> → y=0
// <h1>Text</h1> → y=1 (body=1)
// <h2> → y=2 (body=1 + h1=1)
// <a>Link1</a> → y=3 (body=1 + h1=1 + h2=1)
// </h2>
// <p>Text</p> → y=5 (body=1 + h1=1 + h2=2)
// <h2> → y=6 (body=1 + h1=1 + h2=2 + p=1)
// <a>Link2</a> → y=7 (body=1 + h1=1 + h2=2 + p=1 + h2=1)
// </h2>
// </body>
//
// Trade-offs:
// - O(depth × siblings × subtree_height) - only left-side traversal
// - Linear scaling: 5px per node
// - Perfect document order, guaranteed unique positions
// - Compact coordinates (1000 nodes ≈ 5,000px)
fn calculateDocumentPosition(node: *Node) f64 {
var position: f64 = 0.0;
var current = node;
// Walk up to root, counting preceding nodes
while (current.parentNode()) |parent| {
// Count all previous siblings and their descendants
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
position += countSubtreeNodes(s);
sibling = s.nextSibling();
}
// Count the parent itself
position += 1.0;
current = parent;
}
return position * 5.0; // 5px per node
}
// Counts total nodes in a subtree (node + all descendants)
fn countSubtreeNodes(node: *Node) f64 {
var count: f64 = 1.0; // Count this node
var child = node.firstChild();
while (child) |c| {
count += countSubtreeNodes(c);
child = c.nextSibling();
}
return count;
}
// Calculates horizontal position using the same approach as y,
// just scaled differently for visual distinction
fn calculateSiblingPosition(node: *Node) f64 {
var position: f64 = 0.0;
var current = node;
// Walk up to root, counting preceding nodes (same as y)
while (current.parentNode()) |parent| {
// Count all previous siblings and their descendants
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
position += countSubtreeNodes(s);
sibling = s.nextSibling();
}
// Count the parent itself
position += 1.0;
current = parent;
}
return position * 5.0; // 5px per node
}
pub fn getElementsByTagName(self: *Element, tag_name: []const u8, frame: *Frame) !Node.GetElementsByTagNameResult {
return self.asNode().getElementsByTagName(tag_name, frame);
}
pub fn getElementsByTagNameNS(self: *Element, namespace: ?[]const u8, local_name: []const u8, frame: *Frame) !collections.NodeLive(.tag_name_ns) {
return self.asNode().getElementsByTagNameNS(namespace, local_name, frame);
}
pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Frame) !collections.NodeLive(.class_name) {
return self.asNode().getElementsByClassName(class_name, frame);
}
pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
const tag_name = self.getTagNameDump();
const node = try frame.createElementNS(self._namespace, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), frame }) catch |err| {
log.err(.dom, "element.clone.failed", .{ .err = err });
};
if (deep) {
var child_it = self.asNode().childrenIterator();
while (child_it.next()) |child| {
if (try child.cloneNodeForAppending(true, frame)) |cloned_child| {
// We pass `true` to `child_already_connected` as a hacky optimization
// We _know_ this child isn't connected (Because the parent isn't connected)
// setting this to `true` skips all connection checks.
try frame.appendNode(node, cloned_child, .{ .child_already_connected = true });
}
}
}
return node;
}
pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
_ = center_if_needed;
}
const ScrollIntoViewOpts = union {
align_to_top: bool,
obj: js.Object,
};
pub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void {
_ = opts;
}
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
try writer.writeByte('<');
try writer.writeAll(self.getTagNameDump());
if (self._attributes) |attributes| {
var it = attributes.iterator();
while (it.next()) |attr| {
try writer.print(" {f}", .{attr});
}
}
try writer.writeByte('>');
}
fn upperTagName(tag_name: *String, buf: []u8) []const u8 {
if (tag_name.len > buf.len) {
log.info(.dom, "tag.long.name", .{ .name = tag_name.str() });
return tag_name.str();
}
const tag = tag_name.str();
return std.ascii.upperString(buf, tag);
}
pub fn getTag(self: *const Element) Tag {
return switch (self._type) {
.html => |he| switch (he._type) {
.anchor => .anchor,
.area => .area,
.base => .base,
.div => .div,
.dl => .dl,
.embed => .embed,
.form => .form,
.p => .p,
.custom => .custom,
.data => .data,
.datalist => .datalist,
.details => .details,
.dialog => .dialog,
.directory => .directory,
.iframe => .iframe,
.img => .img,
.br => .br,
.button => .button,
.canvas => .canvas,
.fieldset => .fieldset,
.font => .font,
.frameset => .frameset,
.heading => |h| h._tag,
.label => .label,
.legend => .legend,
.li => .li,
.map => .map,
.marquee => .marquee,
.ul => .ul,
.ol => .ol,
.object => .object,
.optgroup => .optgroup,
.output => .output,
.picture => .picture,
.param => .param,
.pre => .pre,
.generic => |g| g._tag,
.media => |m| switch (m._type) {
.audio => .audio,
.video => .video,
.generic => .media,
},
.meter => .meter,
.mod => |m| m._tag,
.progress => .progress,
.quote => |q| q._tag,
.script => .script,
.select => .select,
.slot => .slot,
.source => .source,
.span => .span,
.option => .option,
.table => .table,
.table_caption => .caption,
.table_cell => |tc| tc._tag,
.table_col => |tc| tc._tag,
.table_row => .tr,
.table_section => |ts| ts._tag,
.template => .template,
.textarea => .textarea,
.time => .time,
.track => .track,
.input => .input,
.link => .link,
.meta => .meta,
.hr => .hr,
.style => .style,
.title => .title,
.body => .body,
.html => .html,
.head => .head,
.unknown => .unknown,
},
.svg => |se| switch (se._type) {
.svg => .svg,
.generic => |g| g._tag,
},
};
}
pub const Tag = enum {
address,
anchor,
audio,
area,
aside,
article,
b,
blockquote,
body,
br,
button,
base,
canvas,
caption,
circle,
code,
col,
colgroup,
custom,
data,
datalist,
dd,
details,
del,
dfn,
dialog,
div,
directory,
dl,
dt,
embed,
ellipse,
em,
fieldset,
figure,
frameset,
form,
font,
footer,
g,
h1,
h2,
h3,
h4,
h5,
h6,
head,
header,
heading,
hgroup,
hr,
html,
i,
iframe,
img,
input,
ins,
label,
legend,
li,
line,
link,
main,
map,
marquee,
media,
menu,
meta,
meter,
nav,
noscript,
object,
ol,
optgroup,
option,
output,
p,
path,
param,
picture,
polygon,
polyline,
pre,
progress,
quote,
rect,
s,
script,
section,
select,
slot,
source,
span,
strong,
style,
sub,
summary,
sup,
svg,
table,
time,
tbody,
td,
text,
template,
textarea,
tfoot,
th,
thead,
title,
tr,
track,
ul,
video,
unknown,
// If the tag is "unknown", we can't use the optimized tag matching, but
// need to fallback to the actual tag name
pub fn parseForMatch(lower: []const u8) ?Tag {
const tag = std.meta.stringToEnum(Tag, lower) orelse return null;
return switch (tag) {
.unknown, .custom => null,
else => tag,
};
}
pub fn isBlock(self: Tag) bool {
// zig fmt: off
return switch (self) {
// Semantic Layout
.article, .aside, .footer, .header, .main, .nav, .section,
// Grouping / Containers
.address, .div, .fieldset, .figure, .p,
// Headings
.h1, .h2, .h3, .h4, .h5, .h6,
// Lists
.dl, .ol, .ul,
// Preformatted / Quotes
.blockquote, .pre,
// Tables
.table,
// Other
.hr,
=> true,
else => false,
};
// zig fmt: on
}
pub fn isMetadata(self: Tag) bool {
return switch (self) {
.base, .head, .link, .meta, .noscript, .script, .style, .template, .title => true,
else => false,
};
}
// UA stylesheet display:none defaults per HTML Rendering §15.3.1
// "Hidden elements" (https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements).
// The spec also lists basefont, noembed, noframes, rp; those tags are
// obsolete and not represented in this enum, so they fall through to
// `.unknown`/`.custom` and aren't matched here.
pub fn isHiddenByUaStylesheet(self: Tag) bool {
return switch (self) {
.area,
.base,
.datalist,
.head,
.link,
.meta,
.noscript,
.param,
.script,
.source,
.style,
.template,
.title,
.track,
=> true,
else => false,
};
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(Element);
pub const Meta = struct {
pub const name = "Element";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const tagName = bridge.accessor(_tagName, null, .{});
fn _tagName(self: *Element, frame: *Frame) []const u8 {
return self.getTagNameSpec(&frame.buf);
}
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true });
fn _innerText(self: *Element, frame: *const Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{ .ce_reactions = true });
fn _outerHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getOuterHTML(&buf.writer, frame);
return buf.written();
}
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{ .ce_reactions = true });
fn _innerHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerHTML(&buf.writer, frame);
return buf.written();
}
pub const prefix = bridge.accessor(Element._prefix, null, .{});
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true, .ce_reactions = true });
fn _setAttribute(self: *Element, name: String, value: js.Value, frame: *Frame) !void {
return self.setAttribute(name, .wrap(try value.toStringSlice()), frame);
}
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true, .ce_reactions = true });
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, frame: *Frame) !void {
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), frame);
}
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{ .ce_reactions = true });
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{ .ce_reactions = true });
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{ .ce_reactions = true });
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{ .ce_reactions = true });
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{ .ce_reactions = true });
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{ .ce_reactions = true });
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{ .ce_reactions = true });
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{ .ce_reactions = true });
pub const removeAttribute = bridge.function(Element.removeAttribute, .{ .ce_reactions = true });
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true, .ce_reactions = true });
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true, .ce_reactions = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true });
const ShadowRootInit = struct {
mode: String,
};
fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot {
return self.attachShadow(init.mode, frame);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true, .ce_reactions = true });
pub const remove = bridge.function(Element.remove, .{ .ce_reactions = true });
pub const append = bridge.function(Element.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true, .ce_reactions = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true, .ce_reactions = true });
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
pub const getAnimations = bridge.function(Element.getAnimations, .{});
pub const animate = bridge.function(Element.animate, .{});
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});
pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});
pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});
pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});
pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});
pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});
pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});
pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});
pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});
pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});
pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});
pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});
pub const getClientRects = bridge.function(Element.getClientRects, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{});
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
pub const children = bridge.accessor(Element.getChildren, null, .{});
pub const focus = bridge.function(Element.focus, .{});
pub const blur = bridge.function(Element.blur, .{});
pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
};
pub const Build = struct {
// Calls `func_name` with `args` on the most specific type where it is
// implement. This could be on the Element itself.
pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool {
inline for (@typeInfo(Element.Type).@"union".fields) |f| {
if (@field(Element.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, "call")) {
const sub = @field(self._type, f.name);
return S.Build.call(sub, func_name, args);
}
// The inner type implements this function. Call it and we're done.
if (@hasDecl(f.type, func_name)) {
return @call(.auto, @field(f.type, func_name), args);
}
}
}
}
if (@hasDecl(Element.Build, func_name)) {
// Our last resort - the element implements this function.
try @call(.auto, @field(Element.Build, func_name), args);
return true;
}
// inform our caller (the Node) 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: Element" {
try testing.htmlRunner("element", .{});
}