diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index c2c2b17e..7158231f 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -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
diff --git a/src/browser/Session.zig b/src/browser/Session.zig
index 0f90a82a..cacd6f0e 100644
--- a/src/browser/Session.zig
+++ b/src/browser/Session.zig
@@ -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;
diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig
index efb696ec..f4b32ddb 100644
--- a/src/browser/js/Caller.zig
+++ b/src/browser/js/Caller.zig
@@ -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);
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 1b0c768f..ce497e48 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -1176,7 +1176,6 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
return error.FailedToResolvePromise;
}
self.runMicrotasks();
-
return resolver.getPromise();
}
diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig
index 41d8fa2c..4ab5be8a 100644
--- a/src/browser/js/Function.zig
+++ b/src/browser/js/Function.zig
@@ -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;
}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 026ccffa..0feec8a8 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -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"),
diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig
index 4f993d8d..e6f73668 100644
--- a/src/browser/js/js.zig
+++ b/src/browser/js/js.zig
@@ -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;
}
}
diff --git a/src/browser/tests/element/html/template.html b/src/browser/tests/element/html/template.html
index bc605584..52db20fd 100644
--- a/src/browser/tests/element/html/template.html
+++ b/src/browser/tests/element/html/template.html
@@ -166,3 +166,43 @@
testing.expectEqual('First', inner1.textContent);
}
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/xmlserializer.html b/src/browser/tests/xmlserializer.html
new file mode 100644
index 00000000..edbc60c8
--- /dev/null
+++ b/src/browser/tests/xmlserializer.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig
index 67093579..46a5afbe 100644
--- a/src/browser/webapi/Window.zig
+++ b/src/browser/webapi/Window.zig
@@ -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 {
diff --git a/src/browser/webapi/XMLSerializer.zig b/src/browser/webapi/XMLSerializer.zig
new file mode 100644
index 00000000..bbd89a80
--- /dev/null
+++ b/src/browser/webapi/XMLSerializer.zig
@@ -0,0 +1,56 @@
+// 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 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", .{});
+}
diff --git a/src/browser/webapi/element/html/Template.zig b/src/browser/webapi/element/html/Template.zig
index 4529230f..8d8dc0b2 100644
--- a/src/browser/webapi/element/html/Template.zig
+++ b/src/browser/webapi/element/html/Template.zig
@@ -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 {
diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig
index 35056eb1..b00b3c7c 100644
--- a/src/browser/webapi/net/Fetch.zig
+++ b/src/browser/webapi/net/Fetch.zig
@@ -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);
}
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index 9e79072b..24447566 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -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, .{});