mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-26 10:23:10 -04:00
872 lines
28 KiB
Zig
872 lines
28 KiB
Zig
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 reflect = @import("../reflect.zig");
|
|
|
|
const Node = @import("Node.zig");
|
|
const collections = @import("collections.zig");
|
|
const Selector = @import("selector/Selector.zig");
|
|
pub const Attribute = @import("element/Attribute.zig");
|
|
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
|
|
pub const DOMStringMap = @import("element/DOMStringMap.zig");
|
|
const DOMRect = @import("DOMRect.zig");
|
|
const css = @import("css.zig");
|
|
|
|
pub const Svg = @import("element/Svg.zig");
|
|
pub const Html = @import("element/Html.zig");
|
|
|
|
const Element = @This();
|
|
|
|
pub const Namespace = enum(u8) {
|
|
html,
|
|
svg,
|
|
mathml,
|
|
xml,
|
|
|
|
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",
|
|
};
|
|
}
|
|
};
|
|
|
|
_type: Type,
|
|
_proto: *Node,
|
|
_namespace: Namespace = .html,
|
|
_attributes: ?*Attribute.List = null,
|
|
_style: ?*CSSStyleProperties = null,
|
|
_class_list: ?*collections.DOMTokenList = 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 asConstNode(self: *const Element) *const Node {
|
|
return self._proto;
|
|
}
|
|
|
|
pub fn className(self: *const Element) []const u8 {
|
|
return switch (self._type) {
|
|
inline else => |c| return c.className(),
|
|
};
|
|
}
|
|
|
|
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",
|
|
.body => "body",
|
|
.br => "br",
|
|
.button => "button",
|
|
.custom => |e| e._tag_name.str(),
|
|
.div => "div",
|
|
.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",
|
|
.li => "li",
|
|
.link => "link",
|
|
.meta => "meta",
|
|
.ol => "ol",
|
|
.option => "option",
|
|
.p => "p",
|
|
.script => "script",
|
|
.select => "select",
|
|
.style => "style",
|
|
.text_area => "textarea",
|
|
.title => "title",
|
|
.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 {
|
|
switch (self._type) {
|
|
.html => |he| switch (he._type) {
|
|
.custom => |e| {
|
|
@branchHint(.unlikely);
|
|
return upperTagName(&e._tag_name, buf);
|
|
},
|
|
else => return switch (he._type) {
|
|
.anchor => "A",
|
|
.body => "BODY",
|
|
.br => "BR",
|
|
.button => "BUTTON",
|
|
.custom => |e| upperTagName(&e._tag_name, buf),
|
|
.div => "DIV",
|
|
.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",
|
|
.li => "LI",
|
|
.link => "LINK",
|
|
.meta => "META",
|
|
.ol => "OL",
|
|
.option => "OPTION",
|
|
.p => "P",
|
|
.script => "SCRIPT",
|
|
.select => "SELECT",
|
|
.style => "STYLE",
|
|
.text_area => "TEXTAREA",
|
|
.title => "TITLE",
|
|
.ul => "UL",
|
|
.unknown => |e| switch (self._namespace) {
|
|
.html => upperTagName(&e._tag_name, buf),
|
|
.svg, .xml, .mathml => return e._tag_name.str(),
|
|
},
|
|
},
|
|
},
|
|
.svg => |svg| return 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 getInnerText(self: *Element, writer: *std.Io.Writer) !void {
|
|
var it = self.asNode().childrenIterator();
|
|
while (it.next()) |child| {
|
|
try child.getTextContent(writer);
|
|
}
|
|
}
|
|
|
|
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer) !void {
|
|
const dump = @import("../dump.zig");
|
|
return dump.deep(self.asNode(), .{}, writer);
|
|
}
|
|
|
|
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer) !void {
|
|
const dump = @import("../dump.zig");
|
|
return dump.children(self.asNode(), .{}, writer);
|
|
}
|
|
|
|
pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
|
|
const parent = self.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 parsing if html is empty
|
|
if (html.len == 0) {
|
|
return;
|
|
}
|
|
|
|
// Parse and add new children
|
|
try page.parseHtmlAsChildren(parent, html);
|
|
}
|
|
|
|
pub fn getId(self: *const Element) []const u8 {
|
|
return self.getAttributeSafe("id") orelse "";
|
|
}
|
|
|
|
pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
|
|
return self.setAttributeSafe("id", value, page);
|
|
}
|
|
|
|
pub fn getClassName(self: *const Element) []const u8 {
|
|
return self.getAttributeSafe("class") orelse "";
|
|
}
|
|
|
|
pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
|
|
return self.setAttributeSafe("class", value, page);
|
|
}
|
|
|
|
pub fn attributeIterator(self: *Element) Attribute.Iterator {
|
|
const attributes = self._attributes orelse return .{};
|
|
return attributes.iterator(self);
|
|
}
|
|
|
|
pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 {
|
|
const attributes = self._attributes orelse return null;
|
|
return attributes.get(name, page);
|
|
}
|
|
|
|
pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool {
|
|
const attributes = self._attributes orelse return false;
|
|
const value = try attributes.get(name, page);
|
|
return value != null;
|
|
}
|
|
|
|
pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute {
|
|
const attributes = self._attributes orelse return null;
|
|
return attributes.getAttribute(name, self, page);
|
|
}
|
|
|
|
pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
|
|
const attributes = self._attributes orelse return null;
|
|
return attributes.getSafe(name);
|
|
}
|
|
|
|
pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
|
|
const attributes = try self.getOrCreateAttributeList(page);
|
|
_ = try attributes.put(name, value, self, page);
|
|
}
|
|
|
|
pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
|
|
const attributes = try self.getOrCreateAttributeList(page);
|
|
_ = try attributes.putSafe(name, value, self, page);
|
|
}
|
|
|
|
fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
|
|
return self._attributes orelse {
|
|
const a = try page.arena.create(Attribute.List);
|
|
a.* = .{};
|
|
self._attributes = a;
|
|
return a;
|
|
};
|
|
}
|
|
|
|
pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {
|
|
if (attr._element) |el| {
|
|
if (el == self) {
|
|
return attr;
|
|
}
|
|
attr._element = null;
|
|
_ = try el.removeAttributeNode(attr, page);
|
|
}
|
|
|
|
const attributes = self._attributes orelse blk: {
|
|
const a = try page.arena.create(Attribute.List);
|
|
a.* = .{};
|
|
self._attributes = a;
|
|
break :blk a;
|
|
};
|
|
return attributes.putAttribute(attr, self, page);
|
|
}
|
|
|
|
pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void {
|
|
const attributes = self._attributes orelse return;
|
|
return attributes.delete(name, self, page);
|
|
}
|
|
|
|
pub fn removeAttributeNode(self: *Element, attr: *Attribute, page: *Page) !*Attribute {
|
|
if (attr._element == null or attr._element.? != self) {
|
|
return error.NotFound;
|
|
}
|
|
try self.removeAttribute(attr._name, page);
|
|
attr._element = null;
|
|
return attr;
|
|
}
|
|
|
|
pub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 {
|
|
const attributes = self._attributes orelse return &.{};
|
|
return attributes.getNames(page);
|
|
}
|
|
|
|
pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap {
|
|
const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self));
|
|
if (!gop.found_existing) {
|
|
const attributes = try self.getOrCreateAttributeList(page);
|
|
const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
|
|
gop.value_ptr.* = named_node_map;
|
|
}
|
|
return gop.value_ptr.*;
|
|
}
|
|
|
|
pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties {
|
|
return self._style orelse blk: {
|
|
const s = try CSSStyleProperties.init(self, page);
|
|
self._style = s;
|
|
break :blk s;
|
|
};
|
|
}
|
|
|
|
pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
|
return self._class_list orelse blk: {
|
|
const cl = try page._factory.create(collections.DOMTokenList{
|
|
._element = self,
|
|
._attribute_name = "class",
|
|
});
|
|
self._class_list = cl;
|
|
break :blk cl;
|
|
};
|
|
}
|
|
|
|
pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
|
|
const gop = try page._element_datasets.getOrPut(page.arena, self);
|
|
if (!gop.found_existing) {
|
|
gop.value_ptr.* = try page._factory.create(DOMStringMap{
|
|
._element = self,
|
|
});
|
|
}
|
|
return gop.value_ptr.*;
|
|
}
|
|
|
|
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
|
page.domChanged();
|
|
var parent = self.asNode();
|
|
|
|
var it = parent.childrenIterator();
|
|
while (it.next()) |child| {
|
|
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
}
|
|
|
|
const parent_is_connected = parent.isConnected();
|
|
for (nodes) |node_or_text| {
|
|
var child_connected = false;
|
|
const child = try node_or_text.toNode(page);
|
|
if (child._parent) |previous_parent| {
|
|
child_connected = child.isConnected();
|
|
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
|
}
|
|
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
|
|
}
|
|
}
|
|
|
|
pub fn remove(self: *Element, page: *Page) void {
|
|
const node = self.asNode();
|
|
const parent = node._parent orelse return;
|
|
page.removeNode(parent, node, .{ .will_be_reconnected = false });
|
|
}
|
|
|
|
pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
|
|
return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page);
|
|
}
|
|
|
|
pub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
|
const parent = self.asNode();
|
|
for (nodes) |node_or_text| {
|
|
const child = try node_or_text.toNode(page);
|
|
_ = try parent.appendChild(child, page);
|
|
}
|
|
}
|
|
|
|
pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
|
const parent = self.asNode();
|
|
var i = nodes.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const child = try nodes[i].toNode(page);
|
|
_ = try parent.insertBefore(child, parent.firstChild(), page);
|
|
}
|
|
}
|
|
|
|
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, page: *Page) !bool {
|
|
return Selector.matches(self, selector, page);
|
|
}
|
|
|
|
pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {
|
|
return Selector.querySelector(self.asNode(), selector, page);
|
|
}
|
|
|
|
pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Selector.List {
|
|
return Selector.querySelectorAll(self.asNode(), input, page);
|
|
}
|
|
|
|
pub fn parentElement(self: *Element) ?*Element {
|
|
return self._proto.parentElement();
|
|
}
|
|
|
|
pub fn checkVisibility(self: *Element, page: *Page) !bool {
|
|
var current: ?*Element = self;
|
|
|
|
while (current) |el| {
|
|
const style = try el.getStyle(page);
|
|
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
|
|
if (std.mem.eql(u8, display, "none")) {
|
|
return false;
|
|
}
|
|
current = el.parentElement();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
|
|
const is_visible = try self.checkVisibility(page);
|
|
if (!is_visible) {
|
|
return page._factory.create(DOMRect{
|
|
._x = 0.0,
|
|
._y = 0.0,
|
|
._width = 0.0,
|
|
._height = 0.0,
|
|
._top = 0.0,
|
|
._right = 0.0,
|
|
._bottom = 0.0,
|
|
._left = 0.0,
|
|
});
|
|
}
|
|
|
|
const y = calculateDocumentPosition(self.asNode());
|
|
|
|
var width: f64 = 1.0;
|
|
var height: f64 = 1.0;
|
|
|
|
const style = try self.getStyle(page);
|
|
const decl = style.asCSSStyleDeclaration();
|
|
width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0;
|
|
height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0;
|
|
|
|
if (width == 1.0 or height == 1.0) {
|
|
const tag = self.getTag();
|
|
if (tag == .img or tag == .iframe) {
|
|
if (self.getAttributeSafe("width")) |w| {
|
|
width = std.fmt.parseFloat(f64, w) catch width;
|
|
}
|
|
if (self.getAttributeSafe("height")) |h| {
|
|
height = std.fmt.parseFloat(f64, h) catch height;
|
|
}
|
|
}
|
|
}
|
|
|
|
const x: f64 = 0.0;
|
|
const top = y;
|
|
const left = x;
|
|
const right = x + width;
|
|
const bottom = y + height;
|
|
|
|
return page._factory.create(DOMRect{
|
|
._x = x,
|
|
._y = y,
|
|
._width = width,
|
|
._height = height,
|
|
._top = top,
|
|
._right = right,
|
|
._bottom = bottom,
|
|
._left = left,
|
|
});
|
|
}
|
|
|
|
// Calculates a pseudo-position in the document using an efficient heuristic.
|
|
//
|
|
// Instead of walking the entire DOM tree (which would be O(total_nodes)), this
|
|
// function walks UP the tree counting previous siblings at each level. Each level
|
|
// uses exponential weighting (1000x per depth level) to preserve document order.
|
|
//
|
|
// This gives O(depth * avg_siblings) complexity while maintaining relative positioning
|
|
// that's useful for scraping and understanding element flow in the document.
|
|
//
|
|
// Example:
|
|
// <body> → position 0
|
|
// <div> → position 0 (0 siblings at level 1)
|
|
// <span></span> → position 0 (0 siblings at level 2)
|
|
// <span></span> → position 1 (1 sibling at level 2)
|
|
// </div>
|
|
// <div> → position 1000 (1 sibling at level 1, weighted by 1000)
|
|
// <p></p> → position 1000 (0 siblings at level 2, parent has 1000)
|
|
// </div>
|
|
// </body>
|
|
//
|
|
// Trade-offs:
|
|
// - Much faster than full tree-walking for deep/large DOMs
|
|
// - Positions reflect document order and parent-child relationships
|
|
// - Not pixel-accurate, but sufficient for 1x1 layout heuristics
|
|
fn calculateDocumentPosition(node: *Node) f64 {
|
|
var position: f64 = 0.0;
|
|
var multiplier: f64 = 1.0;
|
|
var current = node;
|
|
|
|
while (current.parentNode()) |parent| {
|
|
var count: f64 = 0.0;
|
|
var sibling = parent.firstChild();
|
|
while (sibling) |s| {
|
|
if (s == current) break;
|
|
count += 1.0;
|
|
sibling = s.nextSibling();
|
|
}
|
|
|
|
position += count * multiplier;
|
|
multiplier *= 1000.0;
|
|
current = parent;
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
const GetElementsByTagNameResult = union(enum) {
|
|
tag: collections.NodeLive(.tag),
|
|
tag_name: collections.NodeLive(.tag_name),
|
|
};
|
|
pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
|
|
if (tag_name.len > 256) {
|
|
// 256 seems generous.
|
|
return error.InvalidTagName;
|
|
}
|
|
|
|
const lower = std.ascii.lowerString(&page.buf, tag_name);
|
|
if (Tag.parseForMatch(lower)) |known| {
|
|
// optimized for known tag names
|
|
return .{
|
|
.tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page),
|
|
};
|
|
}
|
|
|
|
const arena = page.arena;
|
|
const filter = try String.init(arena, lower, .{});
|
|
return .{ .tag_name = collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) };
|
|
}
|
|
|
|
pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
|
|
const arena = page.arena;
|
|
const filter = try arena.dupe(u8, class_name);
|
|
return collections.NodeLive(.class_name).init(arena, self.asNode(), filter, page);
|
|
}
|
|
|
|
pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node {
|
|
const tag_name = self.getTagNameDump();
|
|
const namespace_uri = self.getNamespaceURI();
|
|
|
|
const node = try page.createElement(namespace_uri, tag_name, self._attributes);
|
|
|
|
if (deep) {
|
|
var child_it = self.asNode().childrenIterator();
|
|
while (child_it.next()) |child| {
|
|
const cloned_child = try child.cloneNode(true, page);
|
|
// We pass `true` to `child_already_connected` as a hacky optimization
|
|
// We _know_ this child isn't connected (Becasue the parent isn't connected)
|
|
// setting this to `true` skips all connection checks and just assumes t
|
|
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
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();
|
|
}
|
|
return std.ascii.upperString(buf, tag_name.str());
|
|
}
|
|
|
|
pub fn getTag(self: *const Element) Tag {
|
|
return switch (self._type) {
|
|
.html => |he| switch (he._type) {
|
|
.anchor => .anchor,
|
|
.div => .div,
|
|
.form => .form,
|
|
.p => .p,
|
|
.custom => .custom,
|
|
.iframe => .iframe,
|
|
.img => .img,
|
|
.br => .br,
|
|
.button => .button,
|
|
.heading => |h| h._tag,
|
|
.li => .li,
|
|
.ul => .ul,
|
|
.ol => .ol,
|
|
.generic => |g| g._tag,
|
|
.script => .script,
|
|
.select => .select,
|
|
.option => .option,
|
|
.text_area => .textarea,
|
|
.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 {
|
|
anchor,
|
|
b,
|
|
body,
|
|
br,
|
|
button,
|
|
circle,
|
|
custom,
|
|
div,
|
|
ellipse,
|
|
em,
|
|
form,
|
|
g,
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6,
|
|
head,
|
|
header,
|
|
heading,
|
|
hr,
|
|
html,
|
|
i,
|
|
iframe,
|
|
img,
|
|
input,
|
|
li,
|
|
line,
|
|
link,
|
|
main,
|
|
meta,
|
|
nav,
|
|
ol,
|
|
option,
|
|
p,
|
|
path,
|
|
polygon,
|
|
polyline,
|
|
rect,
|
|
script,
|
|
select,
|
|
span,
|
|
strong,
|
|
style,
|
|
svg,
|
|
text,
|
|
textarea,
|
|
title,
|
|
ul,
|
|
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 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 tagName = bridge.accessor(_tagName, null, .{});
|
|
fn _tagName(self: *Element, page: *Page) []const u8 {
|
|
return self.getTagNameSpec(&page.buf);
|
|
}
|
|
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
|
|
|
|
pub const innerText = bridge.accessor(_innerText, null, .{});
|
|
fn _innerText(self: *Element, 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 outerHTML = bridge.accessor(_outerHTML, null, .{});
|
|
fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
|
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
|
try self.getOuterHTML(&buf.writer);
|
|
return buf.written();
|
|
}
|
|
|
|
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
|
|
fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
|
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
|
try self.getInnerHTML(&buf.writer);
|
|
return buf.written();
|
|
}
|
|
|
|
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
|
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
|
|
pub const classList = bridge.accessor(Element.getClassList, null, .{});
|
|
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
|
|
pub const style = bridge.accessor(Element.getStyle, null, .{});
|
|
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
|
|
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
|
|
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
|
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
|
|
pub const setAttribute = bridge.function(Element.setAttribute, .{});
|
|
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
|
|
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
|
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
|
|
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
|
|
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
|
|
pub const remove = bridge.function(Element.remove, .{});
|
|
pub const append = bridge.function(Element.append, .{});
|
|
pub const prepend = bridge.function(Element.prepend, .{});
|
|
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 checkVisibility = bridge.function(Element.checkVisibility, .{});
|
|
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
|
|
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
|
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
|
|
pub const children = bridge.accessor(Element.getChildren, null, .{});
|
|
};
|
|
|
|
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", .{});
|
|
}
|