From 34f0857b4fc45fc6b7b59ccfb3a355c4c6873b97 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 12:51:56 +0800 Subject: [PATCH] Element legacy test passing --- src/browser/tests/document/document.html | 77 ++++++++++++++++ src/browser/tests/element/attributes.html | 81 +++++++++++++++++ src/browser/tests/element/element.html | 90 ++++++++++++++++++- src/browser/tests/element/query_selector.html | 2 + .../tests/element/query_selector_all.html | 3 + src/browser/tests/legacy/dom/element.html | 41 +-------- src/browser/webapi/DOMException.zig | 2 +- src/browser/webapi/Document.zig | 31 +++++++ src/browser/webapi/Element.zig | 43 ++++++++- src/browser/webapi/collections/NodeList.zig | 1 + src/browser/webapi/element/Attribute.zig | 26 ++++++ src/browser/webapi/selector/Selector.zig | 3 +- 12 files changed, 356 insertions(+), 44 deletions(-) diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index 1138c132..4ec11411 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -181,3 +181,80 @@ document.cookie = 'IgnoreMy=Ghost; HttpOnly'; testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie); + + + + + + diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index e33af9ed..56f6ef83 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -165,3 +165,84 @@ testing.expectEqual(false, div.hasAttributes()); } + + diff --git a/src/browser/tests/element/element.html b/src/browser/tests/element/element.html index f779c9cd..2a7adbaf 100644 --- a/src/browser/tests/element/element.html +++ b/src/browser/tests/element/element.html @@ -9,7 +9,7 @@ Span 1

Paragraph 2

-
+
+ + + + + + + + diff --git a/src/browser/tests/element/query_selector.html b/src/browser/tests/element/query_selector.html index cb753465..9750bd1a 100644 --- a/src/browser/tests/element/query_selector.html +++ b/src/browser/tests/element/query_selector.html @@ -10,6 +10,8 @@ - - @@ -338,4 +301,4 @@ const p = document.createElement('p'); p.textContent = 'XAnge\xa0Privacy'; testing.expectEqual('

XAnge Privacy

', p.outerHTML); - + --> diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 72d79559..7ae241d2 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -57,7 +57,7 @@ pub fn getName(self: *const DOMException) []const u8 { pub fn getMessage(self: *const DOMException) []const u8 { return switch (self._code) { .none => "", - .invalid_character_error => "Invalid Character", + .invalid_character_error => "Error: Invalid Character", .index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount", .syntax_error => "Syntax Error", .not_supported => "Not Supported", diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 1973b8f0..a665849f 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -125,6 +125,16 @@ pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const return node.as(Element); } +pub fn createAttribute(_: *const Document, name: []const u8, page: *Page) !?*Element.Attribute { + try Element.Attribute.validateAttributeName(name); + return page._factory.node(Element.Attribute{ + ._proto = undefined, + ._name = try page.dupeString(name), + ._value = "", + ._element = null, + }); +} + pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { const id = id_ orelse return null; return self._elements_by_id.get(id); @@ -317,6 +327,24 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !* return node.cloneNode(deep_, page); } +pub fn append(self: *Document, 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: *Document, 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); + } +} + const ReadyState = enum { loading, interactive, @@ -360,6 +388,7 @@ pub const JsApi = struct { pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createComment = bridge.function(Document.createComment, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{}); + pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true }); pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true }); pub const createRange = bridge.function(Document.createRange, .{}); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); @@ -373,6 +402,8 @@ pub const JsApi = struct { pub const getElementsByName = bridge.function(Document.getElementsByName, .{}); pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true }); pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); + pub const append = bridge.function(Document.append, .{}); + pub const prepend = bridge.function(Document.prepend, .{}); pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 4c9287d5..00b46cdf 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -343,6 +343,14 @@ pub fn setId(self: *Element, value: []const u8, page: *Page) !void { return self.setAttributeSafe("id", value, page); } +pub fn getDir(self: *const Element) []const u8 { + return self.getAttributeSafe("dir") orelse ""; +} + +pub fn setDir(self: *Element, value: []const u8, page: *Page) !void { + return self.setAttributeSafe("dir", value, page); +} + pub fn getClassName(self: *const Element) []const u8 { return self.getAttributeSafe("class") orelse ""; } @@ -388,6 +396,7 @@ pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attrib } pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void { + try Attribute.validateAttributeName(name); const attributes = try self.getOrCreateAttributeList(page); _ = try attributes.put(name, value, self, page); } @@ -503,6 +512,7 @@ pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void { } pub fn toggleAttribute(self: *Element, name: []const u8, force: ?bool, page: *Page) !bool { + try Attribute.validateAttributeName(name); const has = try self.hasAttribute(name, page); const should_add = force orelse !has; @@ -647,6 +657,27 @@ pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !voi } } +pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, node, page); + } +} + +pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + pub fn firstElementChild(self: *Element) ?*Element { var maybe_child = self.asNode().firstChild(); while (maybe_child) |child| { @@ -946,6 +977,10 @@ pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node { return node; } +pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void { + _ = center_if_needed; +} + pub fn format(self: *Element, writer: *std.Io.Writer) !void { try writer.writeByte('<'); try writer.writeAll(self.getTagNameDump()); @@ -1136,6 +1171,7 @@ pub const JsApi = struct { pub const localName = bridge.accessor(Element.getLocalName, null, .{}); pub const id = bridge.accessor(Element.getId, Element.setId, .{}); + pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{}); 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, .{}); @@ -1145,10 +1181,10 @@ pub const JsApi = struct { pub const hasAttributes = bridge.function(Element.hasAttributes, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); - pub const setAttribute = bridge.function(Element.setAttribute, .{}); + pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true }); pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{}); pub const removeAttribute = bridge.function(Element.removeAttribute, .{}); - pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{}); + pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true }); pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{}); pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true }); pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); @@ -1167,6 +1203,8 @@ pub const JsApi = struct { pub const remove = bridge.function(Element.remove, .{}); pub const append = bridge.function(Element.append, .{}); pub const prepend = bridge.function(Element.prepend, .{}); + pub const before = bridge.function(Element.before, .{}); + pub const after = bridge.function(Element.after, .{}); pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{}); pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{}); pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{}); @@ -1188,6 +1226,7 @@ pub const JsApi = struct { pub const children = bridge.accessor(Element.getChildren, null, .{}); pub const focus = bridge.function(Element.focus, .{}); pub const blur = bridge.function(Element.blur, .{}); + pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{}); }; pub const Build = struct { diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index dae61509..8ee8b104 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -111,6 +111,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(NodeList.length, null, .{}); pub const @"[]" = bridge.indexed(NodeList.getAtIndex, .{ .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, .{}); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index b5d45a61..d7fa36e6 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -343,6 +343,32 @@ fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool { return node.isConnected(); } +pub fn validateAttributeName(name: []const u8) !void { + if (name.len == 0) { + return error.InvalidCharacterError; + } + + const first = name[0]; + if ((first >= '0' and first <= '9') or first == '-' or first == '.') { + return error.InvalidCharacterError; + } + + for (name) |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: []const u8, page: *Page) ![]const u8 { if (!needsLowerCasing(name)) { return name; diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 44d7c438..6f0869eb 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -38,7 +38,8 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen if (first == .id) { const el = page.getElementByIdFromNode(root, first.id) orelse continue; // Check if the element is within the root subtree - if (root.contains(el.asNode())) { + const node = el.asNode(); + if (node != root and root.contains(node)) { return el; } continue;