From 0beae3b1a67d5f19491763edc6727b4247860286 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 8 Dec 2025 14:22:24 +0800 Subject: [PATCH] Various legacy document tests document.embeds, document.plugins, document.anchor, document.getElementsByName getElementsByClassName support for multiple class names various document getters --- src/browser/Page.zig | 6 + src/browser/js/bridge.zig | 1 + src/browser/tests/document/children.html | 28 ++++ src/browser/tests/document/collections.html | 3 +- .../tests/document/document-title.html | 34 +++++ src/browser/tests/document/document.html | 129 ++++++++++++++++++ .../get_elements_by_class_name-multiple.html | 48 +++++++ .../document/get_elements_by_class_name.html | 4 +- .../tests/document/get_elements_by_name.html | 60 ++++++++ .../get_elements_by_tag_name-wildcard.html | 39 ++++++ src/browser/tests/legacy/dom/document.html | 33 +++-- src/browser/tests/legacy/html/document.html | 2 +- src/browser/webapi/Document.zig | 67 ++++++++- src/browser/webapi/DocumentFragment.zig | 2 +- src/browser/webapi/Element.zig | 36 +++-- src/browser/webapi/HTMLDocument.zig | 33 ++++- .../webapi/collections/HTMLCollection.zig | 20 +++ src/browser/webapi/collections/node_live.zig | 54 ++++++-- src/browser/webapi/element/Attribute.zig | 7 + src/browser/webapi/element/Html.zig | 49 +++---- src/browser/webapi/element/html/Anchor.zig | 12 ++ src/browser/webapi/element/html/Embed.zig | 42 ++++++ src/browser/webapi/element/html/Select.zig | 4 +- 23 files changed, 638 insertions(+), 75 deletions(-) create mode 100644 src/browser/tests/document/children.html create mode 100644 src/browser/tests/document/document-title.html create mode 100644 src/browser/tests/document/get_elements_by_class_name-multiple.html create mode 100644 src/browser/tests/document/get_elements_by_name.html create mode 100644 src/browser/tests/document/get_elements_by_tag_name-wildcard.html create mode 100644 src/browser/webapi/element/html/Embed.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0845b72b..17f25ade 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1120,6 +1120,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined }, ), + asUint("embed") => return self.createHtmlElementT( + Element.Html.Embed, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), else => {}, }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fa622975..faa61a2a 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -531,6 +531,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Data.zig"), @import("../webapi/element/html/Dialog.zig"), @import("../webapi/element/html/Div.zig"), + @import("../webapi/element/html/Embed.zig"), @import("../webapi/element/html/Form.zig"), @import("../webapi/element/html/Generic.zig"), @import("../webapi/element/html/Head.zig"), diff --git a/src/browser/tests/document/children.html b/src/browser/tests/document/children.html new file mode 100644 index 00000000..b8a45d35 --- /dev/null +++ b/src/browser/tests/document/children.html @@ -0,0 +1,28 @@ + + + + + Test + + +
Content
+ + + + + diff --git a/src/browser/tests/document/collections.html b/src/browser/tests/document/collections.html index c9d4c14c..b1c57585 100644 --- a/src/browser/tests/document/collections.html +++ b/src/browser/tests/document/collections.html @@ -5,7 +5,7 @@ - + + + + + + + + + + + + + diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index 5b2e4000..cc5ee34e 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -40,3 +40,132 @@ const emptyText = document.createTextNode(''); testing.expectEqual('', emptyText.nodeValue); + + + + + + +Link 1 +Link 2 +Anchor 1 +Anchor 2 +Both href and name +No attributes + + + + + + + + diff --git a/src/browser/tests/document/get_elements_by_class_name-multiple.html b/src/browser/tests/document/get_elements_by_class_name-multiple.html new file mode 100644 index 00000000..9f1b0f5b --- /dev/null +++ b/src/browser/tests/document/get_elements_by_class_name-multiple.html @@ -0,0 +1,48 @@ + + + +
Div 1
+
Div 2
+
Div 3
+
Div 4
+
Div 5
+
Div 6
+ + + + + + + + diff --git a/src/browser/tests/document/get_elements_by_class_name.html b/src/browser/tests/document/get_elements_by_class_name.html index 1175fa55..3e291dce 100644 --- a/src/browser/tests/document/get_elements_by_class_name.html +++ b/src/browser/tests/document/get_elements_by_class_name.html @@ -20,8 +20,8 @@ + + + + + +Section 1 +User Link + + + + + + diff --git a/src/browser/tests/document/get_elements_by_tag_name-wildcard.html b/src/browser/tests/document/get_elements_by_tag_name-wildcard.html new file mode 100644 index 00000000..685278df --- /dev/null +++ b/src/browser/tests/document/get_elements_by_tag_name-wildcard.html @@ -0,0 +1,39 @@ + + + + + Test + + +
+ Text +
+

Paragraph

+ + + diff --git a/src/browser/tests/legacy/dom/document.html b/src/browser/tests/legacy/dom/document.html index 950daaab..822134d0 100644 --- a/src/browser/tests/legacy/dom/document.html +++ b/src/browser/tests/legacy/dom/document.html @@ -25,7 +25,6 @@ testing.expectEqual(true, newdoc.compatMode === document.compatMode); testing.expectEqual(true, newdoc.characterSet === document.characterSet); testing.expectEqual(true, newdoc.charset === document.charset); - testing.expectEqual(true, newdoc.contentType === document.contentType); testing.expectEqual('HTML', document.documentElement.tagName); @@ -35,8 +34,8 @@ testing.expectEqual('CSS1Compat', document.compatMode); testing.expectEqual('text/html', document.contentType); - testing.expectEqual('http://localhost:9582/src/tests/dom/document.html', document.documentURI); - testing.expectEqual('http://localhost:9582/src/tests/dom/document.html', document.URL); + testing.expectEqual('http://localhost:9589/dom/document.html', document.documentURI); + testing.expectEqual('http://localhost:9589/dom/document.html', document.URL); testing.expectEqual(document.body, document.activeElement); @@ -61,7 +60,7 @@ let byTagNameAll = document.getElementsByTagName('*'); // If you add a script block (or change the HTML in any other way on this // page), this test will break. Adjust it accordingly. - testing.expectEqual(21, byTagNameAll.length); + testing.expectEqual(12, byTagNameAll.length); testing.expectEqual('html', byTagNameAll.item(0).localName); testing.expectEqual('SCRIPT', byTagNameAll.item(11).tagName); @@ -170,21 +169,21 @@ diff --git a/src/browser/tests/legacy/html/document.html b/src/browser/tests/legacy/html/document.html index cc02f7c6..003ee820 100644 --- a/src/browser/tests/legacy/html/document.html +++ b/src/browser/tests/legacy/html/document.html @@ -12,7 +12,7 @@ testing.expectEqual('Document', document.__proto__.__proto__.constructor.name); testing.expectEqual('body', document.body.localName); - testing.expectEqual('localhost:9582', document.domain); + testing.expectEqual('localhost', document.domain); testing.expectEqual('', document.referrer); testing.expectEqual('', document.title); testing.expectEqual('body', document.body.localName); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 6cea4987..928b97c0 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -21,6 +21,7 @@ const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const URL = @import("../URL.zig"); const Node = @import("Node.zig"); const Element = @import("Element.zig"); @@ -79,6 +80,29 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 { return page.url; } +pub fn getContentType(self: *const Document) []const u8 { + return switch (self._type) { + .html => "text/html", + .generic => "application/xml", + }; +} + +pub fn getCharacterSet(_: *const Document) []const u8 { + return "UTF-8"; +} + +pub fn getCompatMode(_: *const Document) []const u8 { + return "CSS1Compat"; +} + +pub fn getReferrer(_: *const Document) []const u8 { + return ""; +} + +pub fn getDomain(_: *const Document, page: *const Page) []const u8 { + return URL.getHostname(page.url); +} + const CreateElementOptions = struct { is: ?[]const u8 = null, }; @@ -109,6 +133,7 @@ pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { const GetElementsByTagNameResult = union(enum) { tag: collections.NodeLive(.tag), tag_name: collections.NodeLive(.tag_name), + all_elements: collections.NodeLive(.all_elements), }; pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult { if (tag_name.len > 256) { @@ -116,23 +141,47 @@ pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) return error.InvalidTagName; } + // Handle wildcard '*' - return all elements + if (std.mem.eql(u8, tag_name, "*")) { + return .{ + .all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, page), + }; + } + const lower = std.ascii.lowerString(&page.buf, tag_name); if (Node.Element.Tag.parseForMatch(lower)) |known| { // optimized for known tag names, comparis return .{ - .tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page), + .tag = collections.NodeLive(.tag).init(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) }; + return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) }; } pub fn getElementsByClassName(self: *Document, 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); + + // Parse space-separated class names + var class_names: std.ArrayList([]const u8) = .empty; + var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); + while (it.next()) |name| { + try class_names.append(arena, try page.dupeString(name)); + } + + return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); +} + +pub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) { + const arena = page.arena; + const filter = try arena.dupe(u8, name); + return collections.NodeLive(.name).init(self.asNode(), filter, page); +} + +pub fn getChildren(self: *Document, page: *Page) !collections.NodeLive(.child_elements) { + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn getDocumentElement(self: *Document) ?*Element { @@ -285,11 +334,20 @@ pub const JsApi = struct { } pub const URL = bridge.accessor(Document.getURL, null, .{}); + pub const documentURI = bridge.accessor(Document.getURL, null, .{}); pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{}); + pub const children = bridge.accessor(Document.getChildren, null, .{}); pub const readyState = bridge.accessor(Document.getReadyState, null, .{}); pub const implementation = bridge.accessor(Document.getImplementation, null, .{}); pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{}); pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{}); + pub const contentType = bridge.accessor(Document.getContentType, null, .{}); + pub const characterSet = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const charset = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const inputEncoding = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{}); + pub const referrer = bridge.accessor(Document.getReferrer, null, .{}); + pub const domain = bridge.accessor(Document.getDomain, null, .{}); pub const createElement = bridge.function(Document.createElement, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{}); pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); @@ -304,6 +362,7 @@ pub const JsApi = struct { pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{}); + 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 }); diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 6c712f55..5e94f3ab 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -94,7 +94,7 @@ pub fn querySelectorAll(self: *DocumentFragment, input: []const u8, page: *Page) } pub fn getChildren(self: *DocumentFragment, page: *Page) !collections.NodeLive(.child_elements) { - return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page); + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn firstElementChild(self: *DocumentFragment) ?*Element { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 758bb1f2..b958f552 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -133,6 +133,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .data => "data", .dialog => "dialog", .div => "div", + .embed => "embed", .form => "form", .generic => |e| e._tag_name.str(), .heading => |e| e._tag_name.str(), @@ -179,6 +180,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .data => "DATA", .dialog => "DIALOG", .div => "DIV", + .embed => "EMBED", .form => "FORM", .generic => |e| upperTagName(&e._tag_name, buf), .heading => |e| upperTagName(&e._tag_name, buf), @@ -305,12 +307,22 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con return attributes.get(name, 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 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 hasAttributeSafe(self: *const Element, name: []const u8) bool { + const attributes = self._attributes orelse return false; + return attributes.hasSafe(name); +} + pub fn hasAttributes(self: *const Element) bool { const attributes = self._attributes orelse return false; return attributes.isEmpty() == false; @@ -321,11 +333,6 @@ pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attrib 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); @@ -506,7 +513,7 @@ pub fn blur(self: *Element, page: *Page) !void { } pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) { - return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page); + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { @@ -750,19 +757,26 @@ pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) ! if (Tag.parseForMatch(lower)) |known| { // optimized for known tag names return .{ - .tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page), + .tag = collections.NodeLive(.tag).init(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) }; + return .{ .tag_name = collections.NodeLive(.tag_name).init(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); + + // Parse space-separated class names + var class_names: std.ArrayList([]const u8) = .empty; + var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); + while (it.next()) |name| { + try class_names.append(arena, name); + } + + return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); } pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node { @@ -819,6 +833,7 @@ pub fn getTag(self: *const Element) Tag { .html => |he| switch (he._type) { .anchor => .anchor, .div => .div, + .embed => .embed, .form => .form, .p => .p, .custom => .custom, @@ -868,6 +883,7 @@ pub const Tag = enum { data, dialog, div, + embed, ellipse, em, form, diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 5e22ecff..16914b7b 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -18,6 +18,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const String = @import("../../string.zig").String; const Page = @import("../Page.zig"); const Node = @import("Node.zig"); @@ -91,22 +92,40 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void { return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page); } } + + const title_node = try page.createElement(null, "title", null); + const title_element = title_node.as(Element); + try title_element.replaceChildren(&.{.{ .text = title }}, page); + _ = try head.asNode().appendChild(title_node, page); } pub fn getImages(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .img, page); + return collections.NodeLive(.tag).init(self.asNode(), .img, page); } pub fn getScripts(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .script, page); + return collections.NodeLive(.tag).init(self.asNode(), .script, page); } -pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .anchor, page); +pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.links) { + return collections.NodeLive(.links).init(self.asNode(), {}, page); +} + +pub fn getAnchors(self: *HTMLDocument, page: *Page) !collections.NodeLive(.anchors) { + return collections.NodeLive(.anchors).init(self.asNode(), {}, page); } pub fn getForms(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .form, page); + return collections.NodeLive(.tag).init(self.asNode(), .form, page); +} + +pub fn getEmbeds(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { + return collections.NodeLive(.tag).init(self.asNode(), .embed, page); +} + +const applet_string = String.init(undefined, "applet", .{}) catch unreachable; +pub fn getApplets(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag_name) { + return collections.NodeLive(.tag_name).init(self.asNode(), applet_string, page); } pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script { @@ -143,7 +162,11 @@ pub const JsApi = struct { pub const images = bridge.accessor(HTMLDocument.getImages, null, .{}); pub const scripts = bridge.accessor(HTMLDocument.getScripts, null, .{}); pub const links = bridge.accessor(HTMLDocument.getLinks, null, .{}); + pub const anchors = bridge.accessor(HTMLDocument.getAnchors, null, .{}); pub const forms = bridge.accessor(HTMLDocument.getForms, null, .{}); + pub const embeds = bridge.accessor(HTMLDocument.getEmbeds, null, .{}); + pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{}); + pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{}); pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{}); pub const location = bridge.accessor(HTMLDocument.getLocation, null, .{ .cache = "location" }); pub const all = bridge.accessor(HTMLDocument.getAll, null, .{}); diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 54c99bff..3160524d 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -28,9 +28,13 @@ const Mode = enum { tag, tag_name, class_name, + name, + all_elements, child_elements, child_tag, selected_options, + links, + anchors, }; const HTMLCollection = @This(); @@ -39,9 +43,13 @@ data: union(Mode) { tag: NodeLive(.tag), tag_name: NodeLive(.tag_name), class_name: NodeLive(.class_name), + name: NodeLive(.name), + all_elements: NodeLive(.all_elements), child_elements: NodeLive(.child_elements), child_tag: NodeLive(.child_tag), selected_options: NodeLive(.selected_options), + links: NodeLive(.links), + anchors: NodeLive(.anchors), }, pub fn length(self: *HTMLCollection, page: *const Page) u32 { @@ -69,9 +77,13 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator { .tag => |*impl| .{ .tag = impl._tw.clone() }, .tag_name => |*impl| .{ .tag_name = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() }, + .name => |*impl| .{ .name = impl._tw.clone() }, + .all_elements => |*impl| .{ .all_elements = impl._tw.clone() }, .child_elements => |*impl| .{ .child_elements = impl._tw.clone() }, .child_tag => |*impl| .{ .child_tag = impl._tw.clone() }, .selected_options => |*impl| .{ .selected_options = impl._tw.clone() }, + .links => |*impl| .{ .links = impl._tw.clone() }, + .anchors => |*impl| .{ .anchors = impl._tw.clone() }, }, }, page); } @@ -83,9 +95,13 @@ pub const Iterator = GenericIterator(struct { tag: TreeWalker.FullExcludeSelf, tag_name: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf, + name: TreeWalker.FullExcludeSelf, + all_elements: TreeWalker.FullExcludeSelf, child_elements: TreeWalker.Children, child_tag: TreeWalker.Children, selected_options: TreeWalker.Children, + links: TreeWalker.FullExcludeSelf, + anchors: TreeWalker.FullExcludeSelf, }, pub fn next(self: *@This(), _: *Page) ?*Element { @@ -93,9 +109,13 @@ pub const Iterator = GenericIterator(struct { .tag => |*impl| impl.nextTw(&self.tw.tag), .tag_name => |*impl| impl.nextTw(&self.tw.tag_name), .class_name => |*impl| impl.nextTw(&self.tw.class_name), + .name => |*impl| impl.nextTw(&self.tw.name), + .all_elements => |*impl| impl.nextTw(&self.tw.all_elements), .child_elements => |*impl| impl.nextTw(&self.tw.child_elements), .child_tag => |*impl| impl.nextTw(&self.tw.child_tag), .selected_options => |*impl| impl.nextTw(&self.tw.selected_options), + .links => |*impl| impl.nextTw(&self.tw.links), + .anchors => |*impl| impl.nextTw(&self.tw.anchors), }; } }, null); diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index ee123b4c..f3f4dd1a 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -35,18 +35,26 @@ const Mode = enum { tag, tag_name, class_name, + name, + all_elements, child_elements, child_tag, selected_options, + links, + anchors, }; const Filters = union(Mode) { tag: Element.Tag, tag_name: String, - class_name: []const u8, + class_name: [][]const u8, + name: []const u8, + all_elements, child_elements, child_tag: Element.Tag, selected_options, + links, + anchors, fn TypeOf(comptime mode: Mode) type { @setEvalBranchQuota(2000); @@ -74,7 +82,7 @@ const Filters = union(Mode) { pub fn NodeLive(comptime mode: Mode) type { const Filter = Filters.TypeOf(mode); const TW = switch (mode) { - .tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf, + .tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors => TreeWalker.FullExcludeSelf, .child_elements, .child_tag, .selected_options => TreeWalker.Children, }; return struct { @@ -83,16 +91,11 @@ pub fn NodeLive(comptime mode: Mode) type { _last_index: usize, _last_length: ?u32, _cached_version: usize, - // NodeLive doesn't use an arena directly, but the filter might have - // used it (to own the string). So we take ownership of the arena so that - // we can free it when we're freed.s - _arena: ?Allocator, const Self = @This(); - pub fn init(arena: ?Allocator, root: *Node, filter: Filter, page: *Page) Self { + pub fn init(root: *Node, filter: Filter, page: *Page) Self { return .{ - ._arena = arena, ._last_index = 0, ._last_length = null, ._filter = filter, @@ -212,10 +215,25 @@ pub fn NodeLive(comptime mode: Mode) type { return std.mem.eql(u8, element_tag, self._filter.str()); }, .class_name => { + if (self._filter.len == 0) { + return false; + } + const el = node.is(Element) orelse return false; const class_attr = el.getAttributeSafe("class") orelse return false; - return Selector.classAttributeContains(class_attr, self._filter); + for (self._filter) |class_name| { + if (!Selector.classAttributeContains(class_attr, class_name)) { + return false; + } + } + return true; }, + .name => { + const el = node.is(Element) orelse return false; + const name_attr = el.getAttributeSafe("name") orelse return false; + return std.mem.eql(u8, name_attr, self._filter); + }, + .all_elements => return node._type == .element, .child_elements => return node._type == .element, .child_tag => { const el = node.is(Element) orelse return false; @@ -227,6 +245,20 @@ pub fn NodeLive(comptime mode: Mode) type { const opt = el.is(Option) orelse return false; return opt.getSelected(); }, + .links => { + // Links are elements with href attribute (TODO: also when implemented) + const el = node.is(Element) orelse return false; + const Anchor = Element.Html.Anchor; + if (el.is(Anchor) == null) return false; + return el.hasAttributeSafe("href"); + }, + .anchors => { + // Anchors are elements with name attribute + const el = node.is(Element) orelse return false; + const Anchor = Element.Html.Anchor; + if (el.is(Anchor) == null) return false; + return el.hasAttributeSafe("name"); + }, } } @@ -249,9 +281,13 @@ pub fn NodeLive(comptime mode: Mode) type { .tag => HTMLCollection{ .data = .{ .tag = self } }, .tag_name => HTMLCollection{ .data = .{ .tag_name = self } }, .class_name => HTMLCollection{ .data = .{ .class_name = self } }, + .name => HTMLCollection{ .data = .{ .name = self } }, + .all_elements => HTMLCollection{ .data = .{ .all_elements = self } }, .child_elements => HTMLCollection{ .data = .{ .child_elements = self } }, .child_tag => HTMLCollection{ .data = .{ .child_tag = self } }, .selected_options => HTMLCollection{ .data = .{ .selected_options = self } }, + .links => HTMLCollection{ .data = .{ .links = self } }, + .anchors => HTMLCollection{ .data = .{ .anchors = self } }, }; return page._factory.create(collection); } diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 1919a24e..b5d45a61 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -135,6 +135,11 @@ pub const List = struct { return entry._value.str(); } + // meant for internal usage, where the name is known to be properly cased + pub fn hasSafe(self: *const List, name: []const u8) bool { + return self.getEntryWithNormalizedName(name) != null; + } + pub fn getAttribute(self: *const List, name: []const u8, element: ?*Element, page: *Page) !?*Attribute { const entry = (try self.getEntry(name, page)) orelse return null; const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry)); @@ -184,6 +189,7 @@ pub const List = struct { }; try page.addElementId(parent, element, entry._value.str()); } + page.domChanged(); page.attributeChange(element, result.normalized, entry._value.str(), old_value); return entry; } @@ -242,6 +248,7 @@ pub const List = struct { page.removeElementId(element, entry._value.str()); } + page.domChanged(); page.attributeRemove(element, result.normalized, old_value); _ = page._attribute_lookup.remove(@intFromPtr(entry)); self._list.remove(&entry._node); diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index cefdaf65..341fbc94 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -23,38 +23,39 @@ const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); const Element = @import("../Element.zig"); -pub const BR = @import("html/BR.zig"); -pub const HR = @import("html/HR.zig"); -pub const LI = @import("html/LI.zig"); -pub const OL = @import("html/OL.zig"); -pub const UL = @import("html/UL.zig"); -pub const Div = @import("html/Div.zig"); -pub const Html = @import("html/Html.zig"); -pub const Head = @import("html/Head.zig"); -pub const Meta = @import("html/Meta.zig"); -pub const Body = @import("html/Body.zig"); -pub const Link = @import("html/Link.zig"); -pub const Image = @import("html/Image.zig"); -pub const Input = @import("html/Input.zig"); -pub const Title = @import("html/Title.zig"); -pub const Style = @import("html/Style.zig"); -pub const Custom = @import("html/Custom.zig"); -pub const Script = @import("html/Script.zig"); pub const Anchor = @import("html/Anchor.zig"); +pub const Body = @import("html/Body.zig"); +pub const BR = @import("html/BR.zig"); pub const Button = @import("html/Button.zig"); +pub const Custom = @import("html/Custom.zig"); pub const Data = @import("html/Data.zig"); pub const Dialog = @import("html/Dialog.zig"); +pub const Div = @import("html/Div.zig"); +pub const Embed = @import("html/Embed.zig"); pub const Form = @import("html/Form.zig"); -pub const Heading = @import("html/Heading.zig"); -pub const Unknown = @import("html/Unknown.zig"); pub const Generic = @import("html/Generic.zig"); -pub const Template = @import("html/Template.zig"); -pub const TextArea = @import("html/TextArea.zig"); +pub const Head = @import("html/Head.zig"); +pub const Heading = @import("html/Heading.zig"); +pub const HR = @import("html/HR.zig"); +pub const Html = @import("html/Html.zig"); +pub const IFrame = @import("html/IFrame.zig"); +pub const Image = @import("html/Image.zig"); +pub const Input = @import("html/Input.zig"); +pub const LI = @import("html/LI.zig"); +pub const Link = @import("html/Link.zig"); +pub const Meta = @import("html/Meta.zig"); +pub const OL = @import("html/OL.zig"); +pub const Option = @import("html/Option.zig"); pub const Paragraph = @import("html/Paragraph.zig"); +pub const Script = @import("html/Script.zig"); pub const Select = @import("html/Select.zig"); pub const Slot = @import("html/Slot.zig"); -pub const Option = @import("html/Option.zig"); -pub const IFrame = @import("html/IFrame.zig"); +pub const Style = @import("html/Style.zig"); +pub const Template = @import("html/Template.zig"); +pub const TextArea = @import("html/TextArea.zig"); +pub const Title = @import("html/Title.zig"); +pub const UL = @import("html/UL.zig"); +pub const Unknown = @import("html/Unknown.zig"); const HtmlElement = @This(); @@ -76,6 +77,7 @@ pub const Type = union(enum) { data: *Data, dialog: *Dialog, div: *Div, + embed: *Embed, form: *Form, generic: *Generic, heading: *Heading, @@ -120,6 +122,7 @@ pub fn className(self: *const HtmlElement) []const u8 { return switch (self._type) { .anchor => "[object HtmlAnchorElement]", .div => "[object HtmlDivElement]", + .embed => "[object HtmlEmbedElement]", .form => "[object HTMLFormElement]", .p => "[object HtmlParagraphElement]", .custom => "[object CUSTOM-TODO]", diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index 47cbe9d4..d6b85c46 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -31,6 +31,9 @@ _proto: *HtmlElement, pub fn asElement(self: *Anchor) *Element { return self._proto._proto; } +pub fn asConstElement(self: *const Anchor) *const Element { + return self._proto._proto; +} pub fn asNode(self: *Anchor) *Node { return self.asElement().asNode(); } @@ -193,6 +196,14 @@ pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe("type", value, page); } +pub fn getName(self: *const Anchor) []const u8 { + return self.asConstElement().getAttributeSafe("name") orelse ""; +} + +pub fn setName(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("name", value, page); +} + pub fn getText(self: *Anchor, page: *Page) ![:0]const u8 { return self.asNode().getTextContentAlloc(page.call_arena); } @@ -235,6 +246,7 @@ pub const JsApi = struct { pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{}); pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{}); + pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{}); pub const origin = bridge.accessor(Anchor.getOrigin, null, .{}); pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{}); pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{}); diff --git a/src/browser/webapi/element/html/Embed.zig b/src/browser/webapi/element/html/Embed.zig new file mode 100644 index 00000000..a3292cb1 --- /dev/null +++ b/src/browser/webapi/element/html/Embed.zig @@ -0,0 +1,42 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const js = @import("../../../js/js.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); + +const Embed = @This(); +_proto: *HtmlElement, + +pub fn asElement(self: *Embed) *Element { + return self._proto._proto; +} +pub fn asNode(self: *Embed) *Node { + return self.asElement().asNode(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Embed); + + pub const Meta = struct { + pub const name = "HTMLEmbedElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; +}; diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index 5b3c0b9e..8a5ef4d2 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -185,7 +185,7 @@ pub fn setRequired(self: *Select, required: bool, page: *Page) !void { pub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollection { // For options, we use the child_tag mode to filter only