mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
Add XmlSerializer, add Response.type, tweak HTMLTemplate to redirect some calls to its Content (DocumentFragment)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1176,7 +1176,6 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
self.runMicrotasks();
|
||||
|
||||
return resolver.getPromise();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
131
src/browser/tests/xmlserializer.html
Normal file
131
src/browser/tests/xmlserializer.html
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
56
src/browser/webapi/XMLSerializer.zig
Normal file
56
src/browser/webapi/XMLSerializer.zig
Normal 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", .{});
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
Reference in New Issue
Block a user