Add XmlSerializer, add Response.type, tweak HTMLTemplate to redirect some calls to its Content (DocumentFragment)

This commit is contained in:
Karl Seguin
2025-12-02 00:08:45 +08:00
parent af8970bbb9
commit e807c9b6be
14 changed files with 295 additions and 26 deletions

View File

@@ -182,7 +182,12 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable;
}
self.js.deinit();
// removeContext() will execute the destructor of any type that
// registered a destructor (e.g. XMLHttpRequest).
// Should be called before we deinit the page, because these objects
// could be referencing it.
self._session.executor.removeContext();
self._script_manager.shutdown = true;
self._session.browser.http_client.abort();
self._script_manager.deinit();
@@ -597,6 +602,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// haven't started navigating, I guess.
return .done;
}
self.js.runMicrotasks();
// Either we have active http connections, or we're in CDP
// mode with an extra socket. Either way, we're waiting

View File

@@ -112,12 +112,6 @@ pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null);
// RemoveJsContext() will execute the destructor of any type that
// registered a destructor (e.g. XMLHttpRequest).
// Should be called before we deinit the page, because these objects
// could be referencing it.
self.executor.removeContext();
self.page.?.deinit();
self.page = null;

View File

@@ -130,6 +130,10 @@ pub fn method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionC
pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: v8.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);

View File

@@ -1176,7 +1176,6 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
return error.FailedToResolvePromise;
}
self.runMicrotasks();
return resolver.getPromise();
}

View File

@@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback;
}

View File

@@ -515,6 +515,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/DOMNodeIterator.zig"),
@import("../webapi/DOMRect.zig"),
@import("../webapi/DOMParser.zig"),
@import("../webapi/XMLSerializer.zig"),
@import("../webapi/NodeFilter.zig"),
@import("../webapi/Element.zig"),
@import("../webapi/element/DOMStringMap.zig"),

View File

@@ -75,22 +75,20 @@ pub const PromiseResolver = struct {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
if (!ok) {
if (self.resolver.resolve(context.v8_context, js_value) == null) {
return error.FailedToResolvePromise;
}
self.runMicrotasks();
}
pub fn reject(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
if (!ok) {
if (self.resolver.reject(context.v8_context, js_value) == null) {
return error.FailedToRejectPromise;
}
self.runMicrotasks();
}
};
@@ -111,9 +109,7 @@ pub const PersistentPromiseResolver = struct {
const js_value = try context.zigValueToJs(value, .{});
defer context.runMicrotasks();
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
if (!ok) {
if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) {
return error.FailedToResolvePromise;
}
}
@@ -124,8 +120,7 @@ pub const PersistentPromiseResolver = struct {
defer context.runMicrotasks();
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
if (!ok) {
if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) {
return error.FailedToRejectPromise;
}
}

View File

@@ -166,3 +166,43 @@
testing.expectEqual('First', inner1.textContent);
}
</script>
<script id=dynamic_content>
{
let template = document.createElement('template');
template.innerHTML = '<p>1</p><span>2</span>';
testing.expectEqual(2, template.content.children.length);
}
</script>
<script id=outerHTML_includes_content>
{
const template = $('#basic');
const outer = template.outerHTML;
testing.expectTrue(outer.includes('<template'));
testing.expectTrue(outer.includes('</template>'));
testing.expectTrue(outer.includes('<div class="container">'));
testing.expectTrue(outer.includes('Hello Template'));
}
</script>
<script id=outerHTML_with_attributes>
{
let template = document.createElement('template');
template.id = 'test-template';
template.innerHTML = '<p>Content</p>';
const outer = template.outerHTML;
testing.expectEqual('<template id="test-template"><p>Content</p></template>', outer);
}
</script>
<script id=textContent_empty>
{
const template = $('#basic');
// textContent on template operates on the template element itself,
// NOT the DocumentFragment, so it should be empty
testing.expectEqual('', template.textContent);
}
</script>

View File

@@ -0,0 +1,131 @@
<!DOCTYPE html>
<script src="testing.js"></script>
<script id=basic>
{
const serializer = new XMLSerializer();
testing.expectEqual('object', typeof serializer);
testing.expectEqual('function', typeof serializer.serializeToString);
}
</script>
<script id=serializeSimpleElement>
{
const serializer = new XMLSerializer();
const div = document.createElement('div');
div.textContent = 'Hello World';
const result = serializer.serializeToString(div);
testing.expectEqual('<div>Hello World</div>', result);
}
</script>
<script id=serializeElementWithAttributes>
{
const serializer = new XMLSerializer();
const div = document.createElement('div');
div.id = 'test';
div.className = 'foo bar';
div.textContent = 'Content';
const result = serializer.serializeToString(div);
testing.expectEqual('<div id="test" class="foo bar">Content</div>', result);
}
</script>
<script id=serializeNestedElements>
{
const serializer = new XMLSerializer();
const div = document.createElement('div');
const p = document.createElement('p');
const span = document.createElement('span');
span.textContent = 'Nested';
p.appendChild(span);
div.appendChild(p);
const result = serializer.serializeToString(div);
testing.expectEqual('<div><p><span>Nested</span></p></div>', result);
}
</script>
<script id=serializeEmptyElement>
{
const serializer = new XMLSerializer();
const div = document.createElement('div');
const result = serializer.serializeToString(div);
testing.expectEqual('<div></div>', result);
}
</script>
<script id=serializeVoidElements>
{
const serializer = new XMLSerializer();
const br = document.createElement('br');
const img = document.createElement('img');
img.src = 'test.png';
const brResult = serializer.serializeToString(br);
testing.expectEqual('<br>', brResult);
const imgResult = serializer.serializeToString(img);
testing.expectEqual('<img src="test.png">', imgResult);
}
</script>
<script id=serializeMultipleSiblings>
{
const serializer = new XMLSerializer();
const container = document.createElement('div');
const span1 = document.createElement('span');
span1.textContent = 'First';
const span2 = document.createElement('span');
span2.textContent = 'Second';
container.appendChild(span1);
container.appendChild(span2);
const result = serializer.serializeToString(container);
testing.expectEqual('<div><span>First</span><span>Second</span></div>', result);
}
</script>
<script id=serializeDocumentFragment>
{
const serializer = new XMLSerializer();
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
div.textContent = 'In fragment';
const span = document.createElement('span');
span.textContent = 'Also in fragment';
fragment.appendChild(div);
fragment.appendChild(span);
const result = serializer.serializeToString(fragment);
testing.expectEqual('<div>In fragment</div><span>Also in fragment</span>', result);
}
</script>
<script id=serializeFromDOM>
{
const serializer = new XMLSerializer();
const testDiv = document.createElement('div');
testDiv.id = 'serialize-test';
testDiv.innerHTML = '<p class="test">Hello <strong>World</strong></p>';
const result = serializer.serializeToString(testDiv);
testing.expectEqual('<div id="serialize-test"><p class="test">Hello <strong>World</strong></p></div>', result);
}
</script>
<script id=roundtripWithInnerHTML>
{
const serializer = new XMLSerializer();
const original = '<div class="container"><p>Text</p><span id="x">More</span></div>';
const div = document.createElement('div');
div.innerHTML = original;
const serialized = serializer.serializeToString(div.firstChild);
testing.expectEqual(original, serialized);
}
</script>

View File

@@ -157,8 +157,8 @@ pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void {
}
}
pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise {
return Fetch.init(input, page);
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.RequestInit, page: *Page) !js.Promise {
return Fetch.init(input, options, page);
}
pub fn setTimeout(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.Object, page: *Page) !u32 {

View File

@@ -0,0 +1,56 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const dump = @import("../dump.zig");
const XMLSerializer = @This();
pub fn init() XMLSerializer {
return .{};
}
pub fn serializeToString(self: *const XMLSerializer, node: *Node, page: *Page) ![]const u8 {
_ = self;
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try dump.deep(node, .{ .shadow = .skip }, &buf.writer, page);
return buf.written();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(XMLSerializer);
pub const Meta = struct {
pub const name = "XMLSerializer";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const constructor = bridge.constructor(XMLSerializer.init, .{});
pub const serializeToString = bridge.function(XMLSerializer.serializeToString, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: XMLSerializer" {
try testing.htmlRunner("xmlserializer.html", .{});
}

View File

@@ -23,6 +23,21 @@ pub fn getContent(self: *Template) *DocumentFragment {
return self._content;
}
pub fn setInnerHTML(self: *Template, html: []const u8, page: *Page) !void {
return self._content.setInnerHTML(html, page);
}
pub fn getOuterHTML(self: *Template, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../../../dump.zig");
const el = self.asElement();
try el.format(writer);
try dump.children(self._content.asNode(), .{ .shadow = .skip }, writer, page);
try writer.writeAll("</");
try writer.writeAll(el.getTagNameDump());
try writer.writeByte('>');
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Template);
@@ -33,6 +48,20 @@ pub const JsApi = struct {
};
pub const content = bridge.accessor(Template.getContent, null, .{});
pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{});
pub const outerHTML = bridge.accessor(_getOuterHTML, null, .{});
fn _getInnerHTML(self: *Template, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self._content.getInnerHTML(&buf.writer, page);
return buf.written();
}
fn _getOuterHTML(self: *Template, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getOuterHTML(&buf.writer, page);
return buf.written();
}
};
pub const Build = struct {

View File

@@ -40,10 +40,11 @@ _response: *Response,
_resolver: js.PersistentPromiseResolver,
pub const Input = Request.Input;
pub const RequestInit = Request.Options;
// @ZIGDOM just enough to get campfire demo working
pub fn init(input: Input, page: *Page) !js.Promise {
const request = try Request.init(input, null, page);
pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise {
const request = try Request.init(input, options, page);
const fetch = try page.arena.create(Fetch);
fetch.* = .{
@@ -60,7 +61,6 @@ pub fn init(input: Input, page: *Page) !js.Promise {
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });
}
std.debug.print("fetch: {s}\n", .{request._url});
try http_client.request(.{
.ctx = fetch,
@@ -100,7 +100,6 @@ fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
fn httpDoneCallback(ctx: *anyopaque) !void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
self._response._body = self._buf.items;
std.debug.print("fetch-resolve: {s}\n", .{self._url});
return self._resolver.resolve(self._response);
}

View File

@@ -26,10 +26,19 @@ const Allocator = std.mem.Allocator;
const Response = @This();
pub const Type = enum {
basic,
cors,
@"error",
@"opaque",
opaqueredirect,
};
_status: u16,
_arena: Allocator,
_headers: *Headers,
_body: ?[]const u8,
_type: Type,
const InitOpts = struct {
status: u16 = 200,
@@ -48,6 +57,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
._status = opts.status,
._body = body,
._headers = opts.headers orelse try Headers.init(page),
._type = .basic, // @ZIGDOM: todo
});
}
@@ -59,6 +69,10 @@ pub fn getHeaders(self: *const Response) *Headers {
return self._headers;
}
pub fn getType(self: *const Response) []const u8 {
return @tagName(self._type);
}
pub fn getBody(self: *const Response, page: *Page) !?*ReadableStream {
const body = self._body orelse return null;
@@ -106,6 +120,7 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(Response.init, .{});
pub const ok = bridge.accessor(Response.isOK, null, .{});
pub const status = bridge.accessor(Response.getStatus, null, .{});
pub const @"type" = bridge.accessor(Response.getType, null, .{});
pub const text = bridge.function(Response.getText, .{});
pub const json = bridge.function(Response.getJson, .{});
pub const headers = bridge.accessor(Response.getHeaders, null, .{});