mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
HTMLDocument
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
131
src/browser/webapi/HTMLDocument.zig
Normal file
131
src/browser/webapi/HTMLDocument.zig
Normal 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" });
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user