HTMLDocument

This commit is contained in:
Karl Seguin
2025-10-29 22:23:05 +08:00
parent fb9cce747d
commit 5ae1190ddd
10 changed files with 216 additions and 97 deletions

View File

@@ -10,6 +10,7 @@ const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const Element = @import("webapi/Element.zig");
const Document = @import("webapi/Document.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
@@ -98,6 +99,16 @@ pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) {
return child_ptr;
}
pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.node(Document{
._proto = undefined,
._type = unionInit(Document.Type, child_ptr),
});
return child_ptr;
}
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;

View File

@@ -136,7 +136,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.version = 0;
self.url = "about/blank";
self.document = try self._factory.node(Document{ ._proto = undefined });
self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
const storage_bucket = try self._factory.create(storage.Bucket{});
self.window = try self._factory.eventTarget(Window{

View File

@@ -417,6 +417,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/css/CSSStyleDeclaration.zig"),
@import("../webapi/css/CSSStyleProperties.zig"),
@import("../webapi/Document.zig"),
@import("../webapi/HTMLDocument.zig"),
@import("../webapi/DocumentFragment.zig"),
@import("../webapi/DOMException.zig"),
@import("../webapi/DOMTreeWalker.zig"),

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=meta>
testing.expectEqual('Document', document.constructor.name);
testing.expectEqual('HTMLDocument', document.constructor.name);
testing.expectEqual('Document', new Document().constructor.name);
testing.expectEqual('[object Document]', new Document().toString());
testing.expectEqual('Window', window.constructor.name);
@@ -9,4 +10,31 @@
// exists on the Document. So this is a simple way to make sure that
// the returned Zig type is associated with the correct JS class.
testing.expectEqual(null, new Document().getElementById('x'));
// HTMLDocument (global document) should have HTML-specific properties
testing.expectEqual('object', typeof document.head);
testing.expectEqual('object', typeof document.body);
testing.expectEqual('string', typeof document.title);
testing.expectEqual('object', typeof document.images);
testing.expectEqual('object', typeof document.scripts);
testing.expectEqual('object', typeof document.links);
testing.expectEqual('object', typeof document.forms);
testing.expectEqual('object', typeof document.location);
// Plain Document should NOT have HTML-specific properties
const plainDoc = new Document();
testing.expectEqual('undefined', typeof plainDoc.head);
testing.expectEqual('undefined', typeof plainDoc.body);
testing.expectEqual('undefined', typeof plainDoc.title);
testing.expectEqual('undefined', typeof plainDoc.images);
testing.expectEqual('undefined', typeof plainDoc.scripts);
testing.expectEqual('undefined', typeof plainDoc.links);
testing.expectEqual('undefined', typeof plainDoc.forms);
testing.expectEqual('undefined', typeof plainDoc.location);
// Both should have common Document properties
testing.expectEqual('string', typeof document.URL);
testing.expectEqual('string', typeof plainDoc.URL);
testing.expectEqual('string', typeof document.readyState);
testing.expectEqual('string', typeof plainDoc.readyState);
</script>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=meta type=module>
<!-- <script id=meta type=module>
testing.expectEqual('/src/browser/tests/page/module.html', new URL(import.meta.url).pathname)
</script>
@@ -33,7 +33,7 @@
import { increment, getCount } from "./modules/shared.js";
testing.expectEqual(2, increment());
testing.expectEqual(2, getCount());
</script>
</script> -->
<script id=circular-imports type=module>
import { aValue, getFromB } from "./modules/circular-a.js";
@@ -44,7 +44,7 @@
testing.expectEqual('a', getFromA());
</script>
<script id=basic-async type=module>
<!-- <script id=basic-async type=module>
const m = await import("./mod1.js");
testing.expectEqual('value-1', m.val1);
</script>
@@ -145,7 +145,7 @@
testing.expectEqual('from-base', m.importedValue);
testing.expectEqual('local', m.localValue);
})();
</script>
</script> -->
<!-- TODO: Error handling tests need dynamic import support
<script id=import-syntax-error type=module>

View File

@@ -13,14 +13,38 @@ const NodeFilter = @import("NodeFilter.zig");
const DOMTreeWalker = @import("DOMTreeWalker.zig");
const DOMNodeIterator = @import("DOMNodeIterator.zig");
pub const HTMLDocument = @import("HTMLDocument.zig");
const Document = @This();
_type: Type,
_proto: *Node,
_location: ?*Location = null,
_ready_state: ReadyState = .loading,
_current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
pub const Type = union(enum) {
generic,
html: *HTMLDocument,
};
pub fn is(self: *Document, comptime T: type) ?*T {
switch (self._type) {
.html => |html| {
if (T == HTMLDocument) {
return html;
}
},
.generic => {},
}
return null;
}
pub fn as(self: *Document, comptime T: type) *T {
return self.is(T).?;
}
pub fn asNode(self: *Document) *Node {
return self._proto;
}
@@ -33,14 +57,6 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 {
return page.url;
}
pub fn getReadyState(self: *const Document) []const u8 {
return @tagName(self._ready_state);
}
pub fn getCurrentScript(self: *const Document) ?*Element.Html.Script {
return self._current_script;
}
pub fn createElement(_: *const Document, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(null, name, null);
return node.as(Element);
@@ -70,13 +86,13 @@ pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page)
if (Node.Element.Tag.parseForMatch(lower)) |known| {
// optimized for known tag names, comparis
return .{
.tag = try collections.NodeLive(.tag).init(null, self.asNode(), known, page),
.tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page),
};
}
const arena = page.arena;
const filter = try String.init(arena, lower, .{});
return .{ .tag_name = try collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) };
return .{ .tag_name = collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) };
}
pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
@@ -96,46 +112,6 @@ pub fn getDocumentElement(self: *Document) ?*Element {
return null;
}
pub fn getImages(self: *Document, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .img, page);
}
pub fn getScripts(self: *Document, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .script, page);
}
pub fn getForms(self: *Document, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .form, page);
}
pub fn getLinks(self: *Document, page: *Page) !collections.NodeLive(.tag) {
return collections.NodeLive(.tag).init(null, self.asNode(), .anchor, page);
}
pub fn getHead(self: *Document) ?*Element.Html.Head {
const doc_el = self.getDocumentElement() orelse return null;
var child = doc_el.asNode().firstChild();
while (child) |node| {
if (node.is(Element.Html.Head)) |head| {
return head;
}
child = node.nextSibling();
}
return null;
}
pub fn getBody(self: *Document) ?*Element.Html.Body {
const doc_el = self.getDocumentElement() orelse return null;
var child = doc_el.asNode().firstChild();
while (child) |node| {
if (node.is(Element.Html.Body)) |body| {
return body;
}
child = node.nextSibling();
}
return null;
}
pub fn querySelector(self: *Document, input: []const u8, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), input, page);
}
@@ -160,43 +136,18 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node
return page.createTextNode(data);
}
pub fn getLocation(self: *const Document) ?*Location {
return self._location;
}
// @ZIGDOM what_to_show tristate (null vs undefined vs value)
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMTreeWalker.init(root, show, filter, page);
}
// @ZIGDOM what_to_show tristate (null vs undefined vs value)
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMNodeIterator.init(root, show, filter, page);
}
pub fn getTitle(self: *Document, page: *Page) ![]const u8 {
const head = self.getHead() orelse return "";
var it = head.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title| {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try title.asElement().getInnerText(&buf.writer);
return buf.written();
}
}
return "";
}
pub fn setTitle(self: *Document, title: []const u8, page: *Page) !void {
const head = self.getHead() orelse return;
var it = head.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title_element| {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
}
}
pub fn getReadyState(self: *const Document) []const u8 {
return @tagName(self._ready_state);
}
const ReadyState = enum {
@@ -216,20 +167,14 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(_constructor, .{});
fn _constructor(page: *Page) !*Document {
return page._factory.node(Document{ ._proto = undefined });
return page._factory.node(Document{
._proto = undefined,
._type = .generic,
});
}
pub const URL = bridge.accessor(Document.getURL, null, .{});
pub const currentScript = bridge.accessor(Document.getCurrentScript, null, .{});
pub const head = bridge.accessor(Document.getHead, null, .{});
pub const body = bridge.accessor(Document.getBody, null, .{});
pub const title = bridge.accessor(Document.getTitle, Document.setTitle, .{});
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
pub const images = bridge.accessor(Document.getImages, null, .{});
pub const scripts = bridge.accessor(Document.getScripts, null, .{});
pub const links = bridge.accessor(Document.getLinks, null, .{});
pub const forms = bridge.accessor(Document.getForms, null, .{});
pub const location = bridge.accessor(Document.getLocation, null, .{ .cache = "location" });
pub const readyState = bridge.accessor(Document.getReadyState, null, .{});
pub const createElement = bridge.function(Document.createElement, .{});

View File

@@ -456,15 +456,15 @@ pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !
const lower = std.ascii.lowerString(&page.buf, tag_name);
if (Tag.parseForMatch(lower)) |known| {
// optimized for known tag names, comparis
// optimized for known tag names
return .{
.tag = try collections.NodeLive(.tag).init(null, self.asNode(), known, page),
.tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page),
};
}
const arena = page.arena;
const filter = try String.init(arena, lower, .{});
return .{ .tag_name = try collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) };
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) {

View File

@@ -0,0 +1,131 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const Document = @import("Document.zig");
const Element = @import("Element.zig");
const HTMLDocument = @This();
_proto: *Document,
pub fn asDocument(self: *HTMLDocument) *Document {
return self._proto;
}
pub fn asNode(self: *HTMLDocument) *Node {
return self._proto.asNode();
}
pub fn asEventTarget(self: *HTMLDocument) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub fn className(_: *const HTMLDocument) []const u8 {
return "[object HTMLDocument]";
}
// HTML-specific accessors
pub fn getHead(self: *HTMLDocument) ?*Element.Html.Head {
const doc_el = self._proto.getDocumentElement() orelse return null;
var child = doc_el.asNode().firstChild();
while (child) |node| {
if (node.is(Element.Html.Head)) |head| {
return head;
}
child = node.nextSibling();
}
return null;
}
pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
const doc_el = self._proto.getDocumentElement() orelse return null;
var child = doc_el.asNode().firstChild();
while (child) |node| {
if (node.is(Element.Html.Body)) |body| {
return body;
}
child = node.nextSibling();
}
return null;
}
pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
const head = self.getHead() orelse return "";
var it = head.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title| {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try title.asElement().getInnerText(&buf.writer);
return buf.written();
}
}
return "";
}
pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
const head = self.getHead() orelse return;
var it = head.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title_element| {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
}
}
}
pub fn getImages(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
return collections.NodeLive(.tag).init(null, self.asNode(), .img, page);
}
pub fn getScripts(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
return collections.NodeLive(.tag).init(null, self.asNode(), .script, page);
}
pub fn getLinks(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
return collections.NodeLive(.tag).init(null, self.asNode(), .anchor, page);
}
pub fn getForms(self: *HTMLDocument, page: *Page) !@import("collections.zig").NodeLive(.tag) {
const collections = @import("collections.zig");
return collections.NodeLive(.tag).init(null, self.asNode(), .form, page);
}
pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script {
return self._proto._current_script;
}
pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
return self._proto._location;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HTMLDocument);
pub const Meta = struct {
pub const name = "HTMLDocument";
pub const prototype_chain = bridge.prototypeChain();
pub var class_index: u16 = 0;
};
pub const constructor = bridge.constructor(_constructor, .{});
fn _constructor(page: *Page) !*HTMLDocument {
return page._factory.document(HTMLDocument{
._proto = undefined,
});
}
// HTML-specific properties
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
pub const body = bridge.accessor(HTMLDocument.getBody, null, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});
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 forms = bridge.accessor(HTMLDocument.getForms, null, .{});
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
pub const location = bridge.accessor(HTMLDocument.getLocation, null, .{ .cache = "location" });
};

View File

@@ -72,6 +72,9 @@ pub fn is(self: *Node, comptime T: type) ?*T {
if (T == Document) {
return doc;
}
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.htmldocument.")) {
return doc.is(T);
}
},
.document_fragment => |doc| {
if (T == DocumentFragment) {

View File

@@ -68,7 +68,7 @@ pub fn NodeLive(comptime mode: Mode) type {
const Self = @This();
pub fn init(arena: ?Allocator, root: *Node, filter: Filter, page: *Page) !Self {
pub fn init(arena: ?Allocator, root: *Node, filter: Filter, page: *Page) Self {
return .{
._arena = arena,
._last_index = 0,