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("'); +} + 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, .{});