mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ac1fa3bc |
@@ -13,8 +13,8 @@
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/1d25fcf3ced688adca3c7a95a138771e4ebba692.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH61eyAwDICIkLAkfQcxsX4TMCKY80QiSUgNBQqx-u",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/bf7ba696b3e819195f8fc349b2778c59aab81a61.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6xm3AwA287seRdWB_mIjZ9_Ayk-81z9uwWoag7Er",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
|
||||
@@ -43,7 +43,6 @@ const Matcher = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Elements = @import("../html/elements.zig");
|
||||
test "matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
@@ -162,7 +161,7 @@ test "matchFirst" {
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
@@ -303,7 +302,7 @@ test "matchAll" {
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
|
||||
@@ -101,7 +101,7 @@ pub const CharacterData = struct {
|
||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
|
||||
if (try parser.nodeType(@ptrCast(self)) != try parser.nodeType(other_node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ const css = @import("css.zig");
|
||||
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const Elements = @import("../html/elements.zig");
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
@@ -46,7 +45,6 @@ pub const Document = struct {
|
||||
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
|
||||
const doc = try parser.documentCreateDocument(
|
||||
try parser.documentHTMLGetTitle(page.window.document),
|
||||
&Elements.createElement,
|
||||
);
|
||||
|
||||
// we have to work w/ document instead of html document.
|
||||
@@ -245,23 +243,17 @@ pub const Document = struct {
|
||||
return try TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
if (state.active_element) |ae| {
|
||||
return try Element.toInterface(ae);
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return @alignCast(@ptrCast(body));
|
||||
return try Element.toInterface(@ptrCast(body));
|
||||
}
|
||||
|
||||
return try parser.documentGetDocumentElement(self);
|
||||
}
|
||||
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
||||
return try Element.toInterface(ae);
|
||||
return get_documentElement(self);
|
||||
}
|
||||
|
||||
// TODO: some elements can't be focused, like if they're disabled
|
||||
@@ -269,7 +261,7 @@ pub const Document = struct {
|
||||
// we could look for the "disabled" attribute, but that's only meaningful
|
||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,8 +30,8 @@ pub const DOMParser = struct {
|
||||
// TODO: Support XML
|
||||
return error.TypeError;
|
||||
}
|
||||
const Elements = @import("../html/elements.zig");
|
||||
return try parser.documentHTMLParseFromStr(string, &Elements.createElement);
|
||||
|
||||
return try parser.documentHTMLParseFromStr(string);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ pub const DOMImplementation = struct {
|
||||
}
|
||||
|
||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
|
||||
const Elements = @import("../html/elements.zig");
|
||||
return try parser.domImplementationCreateHTMLDocument(title, &Elements.createElement);
|
||||
return try parser.domImplementationCreateHTMLDocument(title);
|
||||
}
|
||||
|
||||
pub fn _hasFeature(_: *DOMImplementation) bool {
|
||||
|
||||
@@ -38,7 +38,7 @@ pub const MutationObserver = struct {
|
||||
cbk: Env.Function,
|
||||
arena: Allocator,
|
||||
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// List of records which were observed. When the scopeEnds, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
||||
|
||||
|
||||
@@ -496,7 +496,7 @@ pub const Node = struct {
|
||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
||||
return switch (self) {
|
||||
.node => |n| n,
|
||||
.text => |txt| @alignCast(@ptrCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
.text => |txt| @ptrCast(try parser.documentCreateTextNode(doc, txt)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
|
||||
const testing = std.testing;
|
||||
test "dump.writeHTML" {
|
||||
try parser.init(testing.allocator);
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
try testWriteHTML(
|
||||
@@ -225,8 +225,7 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
var buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
|
||||
const Elements = @import("html/elements.zig");
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src, &Elements.createElement);
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
|
||||
@@ -184,7 +184,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
return @tagName(state.ready_state);
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
@@ -278,7 +278,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
state.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,7 +26,6 @@ const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const State = @import("../State.zig");
|
||||
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
@@ -134,7 +133,7 @@ pub const HTMLElement = struct {
|
||||
try Node.removeChildren(n);
|
||||
|
||||
// attach the text node.
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @alignCast(@ptrCast(t))));
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
|
||||
}
|
||||
|
||||
pub fn _click(e: *parser.ElementHTML) !void {
|
||||
@@ -246,7 +245,7 @@ pub const HTMLAnchorElement = struct {
|
||||
}
|
||||
|
||||
inline fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
return URL.constructor(.{ .element = @alignCast(@ptrCast(self)) }, null, page); // TODO inject base url
|
||||
return URL.constructor(.{ .element = @ptrCast(self) }, null, page); // TODO inject base url
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
@@ -627,85 +626,11 @@ pub const HTMLImageElement = struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub fn createElement(params: [*c]parser.c.dom_html_element_create_params, elem: [*c][*c]parser.ElementHTML) callconv(.c) parser.c.dom_exception {
|
||||
const p: *parser.c.dom_html_element_create_params = @ptrCast(params);
|
||||
switch (p.type) {
|
||||
parser.c.DOM_HTML_ELEMENT_TYPE_INPUT => {
|
||||
return HTMLInputElement.dom_create(params, elem);
|
||||
},
|
||||
else => return parser.c.DOM_NO_ERR,
|
||||
}
|
||||
}
|
||||
|
||||
var input_protected_vtable: parser.c.dom_element_protected_vtable = .{
|
||||
.base = .{
|
||||
.destroy = HTMLInputElement.node_destroy,
|
||||
.copy = HTMLInputElement.node_copy,
|
||||
},
|
||||
.dom_element_parse_attribute = HTMLInputElement.element_parse_attribute,
|
||||
};
|
||||
|
||||
pub const HTMLInputElement = struct {
|
||||
pub const Self = parser.Input;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
base: parser.ElementHTML,
|
||||
|
||||
type: []const u8 = "text",
|
||||
|
||||
pub fn dom_create(params: *parser.c.dom_html_element_create_params, output: *?*parser.ElementHTML) parser.c.dom_exception {
|
||||
var self = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR;
|
||||
output.* = &self.base; // Self can be recovered using @fieldParentPtr
|
||||
|
||||
self.base.base.base.base.vtable = &parser.c._dom_html_element_vtable; // TODO replace get/setAttribute
|
||||
self.base.base.base.vtable = &input_protected_vtable;
|
||||
|
||||
return self.dom_initialise(params);
|
||||
}
|
||||
// Initialise is separated from create such that the leaf type sets the vtable, then calls all the way up the protochain to init
|
||||
pub fn dom_initialise(self: *HTMLInputElement, params: *parser.c.dom_html_element_create_params) parser.c.dom_exception {
|
||||
return parser.c._dom_html_element_initialise(params, &self.base);
|
||||
}
|
||||
|
||||
// This should always be the same and we should not have cleanup for new zig implementation, hopefully
|
||||
pub fn node_destroy(node: [*c]parser.Node) callconv(.c) void {
|
||||
const elem = parser.nodeToHtmlElement(node);
|
||||
parser.c._dom_html_element_finalise(elem);
|
||||
}
|
||||
|
||||
pub fn node_copy(old: [*c]parser.Node, new: [*c][*c]parser.Node) callconv(.c) parser.c.dom_exception {
|
||||
const old_elem = parser.nodeToHtmlElement(old);
|
||||
const self = @as(*HTMLInputElement, @fieldParentPtr("base", old_elem));
|
||||
|
||||
var copy = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR;
|
||||
copy.type = self.type;
|
||||
|
||||
const err = parser.c._dom_html_element_copy_internal(old_elem, ©.base);
|
||||
if (err != parser.c.DOM_NO_ERR) {
|
||||
return err;
|
||||
}
|
||||
|
||||
new.* = @ptrCast(copy);
|
||||
return parser.c.DOM_NO_ERR;
|
||||
}
|
||||
|
||||
// fn ([*c]cimport.struct_dom_element, [*c]cimport.struct_dom_string, [*c]cimport.struct_dom_string, [*c][*c]cimport.struct_dom_string) callconv(.c) c_uint
|
||||
pub fn element_parse_attribute(self: [*c]parser.Element, name: [*c]parser.c.dom_string, value: [*c]parser.c.dom_string, parsed: [*c][*c]parser.c.dom_string) callconv(.c) parser.c.dom_exception {
|
||||
_ = name;
|
||||
_ = self;
|
||||
parsed.* = value;
|
||||
_ = parser.c.dom_string_ref(value);
|
||||
|
||||
// TODO actual implementation
|
||||
// Probably should not use this and instead override the getAttribute setAttribute Element methods directly, perhaps other related functions.
|
||||
|
||||
// handle defaultValue likes
|
||||
// Call setter or store in general attribute store
|
||||
// increment domstring ref?
|
||||
return parser.c.DOM_NO_ERR;
|
||||
}
|
||||
|
||||
pub fn get_defaultValue(self: *parser.Input) ![]const u8 {
|
||||
return try parser.inputGetDefaultValue(self);
|
||||
}
|
||||
@@ -777,26 +702,10 @@ pub const HTMLInputElement = struct {
|
||||
try parser.inputSetSrc(self, new_src);
|
||||
}
|
||||
pub fn get_type(self: *parser.Input) ![]const u8 {
|
||||
const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self)));
|
||||
const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem));
|
||||
|
||||
return input.type;
|
||||
return try parser.inputGetType(self);
|
||||
}
|
||||
pub fn set_type(self: *parser.Input, type_: []const u8) !void {
|
||||
const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self)));
|
||||
const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem));
|
||||
|
||||
const possible_values = [_][]const u8{ "text", "search", "tel", "url", "email", "password", "date", "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", "file", "hidden", "image", "button", "submit", "reset" };
|
||||
var found = false;
|
||||
for (possible_values) |item| {
|
||||
if (std.mem.eql(u8, type_, item)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
input.type = if (found) type_ else "text";
|
||||
|
||||
// TODO DOM events
|
||||
try parser.inputSetType(self, type_);
|
||||
}
|
||||
pub fn get_value(self: *parser.Input) ![]const u8 {
|
||||
return try parser.inputGetValue(self);
|
||||
@@ -1036,22 +945,22 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const state = page.getNodeState(@ptrCast(self)) orelse return null;
|
||||
return state.onload;
|
||||
}
|
||||
|
||||
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
state.onload = function;
|
||||
}
|
||||
|
||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const state = page.getNodeState(@ptrCast(self)) orelse return null;
|
||||
return state.onerror;
|
||||
}
|
||||
|
||||
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
state.onerror = function;
|
||||
}
|
||||
};
|
||||
@@ -1460,7 +1369,9 @@ test "Browser.HTML.HtmlInputElement.propeties.form" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let elem_input = document.querySelector('input')", null },
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
|
||||
}, .{});
|
||||
try runner.testCases(&.{.{ "elem_input.form", "[object HTMLFormElement]" }}, .{}); // Initial value
|
||||
try runner.testCases(&.{
|
||||
.{ "elem_input.form = 'foo'", null },
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
|
||||
}, .{});
|
||||
|
||||
@@ -56,7 +56,7 @@ pub const HTMLSelectElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(select));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
@@ -75,7 +75,7 @@ pub const HTMLSelectElement = struct {
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
||||
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
var state = try page.getOrCreateNodeState(@ptrCast(select));
|
||||
state.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
|
||||
@@ -61,8 +61,7 @@ pub const Window = struct {
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
const Elements = @import("../html/elements.zig");
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement);
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
||||
|
||||
|
||||
@@ -17,29 +17,23 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const c = @cImport({
|
||||
const c = @cImport({
|
||||
@cInclude("dom/dom.h");
|
||||
@cInclude("core/pi.h");
|
||||
@cInclude("dom/bindings/hubbub/parser.h");
|
||||
@cInclude("events/event_target.h");
|
||||
@cInclude("events/event.h");
|
||||
@cInclude("events/mouse_event.h");
|
||||
@cInclude("events/keyboard_event.h");
|
||||
@cInclude("utils/validate.h");
|
||||
@cInclude("html/html_element.h");
|
||||
@cInclude("html/html_document.h");
|
||||
});
|
||||
|
||||
const mimalloc = @import("mimalloc.zig");
|
||||
pub var ARENA: ?Allocator = null;
|
||||
|
||||
// init initializes netsurf lib.
|
||||
// init starts a mimalloc heap arena for the netsurf session. The caller must
|
||||
// call deinit() to free the arena memory.
|
||||
pub fn init(allocator: Allocator) !void {
|
||||
ARENA = allocator;
|
||||
pub fn init() !void {
|
||||
try mimalloc.create();
|
||||
}
|
||||
|
||||
@@ -52,7 +46,6 @@ pub fn deinit() void {
|
||||
c.lwc_deinit_strings();
|
||||
|
||||
mimalloc.destroy();
|
||||
ARENA = null;
|
||||
}
|
||||
|
||||
// Vtable
|
||||
@@ -557,7 +550,7 @@ pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
|
||||
const err = c._dom_mutation_event_get_related_node(evt, &n);
|
||||
try DOMErr(err);
|
||||
if (n == null) return null;
|
||||
return @as(*Node, @alignCast(@ptrCast(n)));
|
||||
return @as(*Node, @ptrCast(n));
|
||||
}
|
||||
|
||||
// EventListener
|
||||
@@ -572,7 +565,7 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
pub fn eventTargetToNode(et: *EventTarget) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(et)));
|
||||
return @as(*Node, @ptrCast(et));
|
||||
}
|
||||
|
||||
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
|
||||
@@ -869,59 +862,6 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
|
||||
return eventDefaultPrevented(@ptrCast(evt));
|
||||
}
|
||||
|
||||
// KeyboardEvent
|
||||
|
||||
pub const KeyboardEvent = c.dom_keyboard_event;
|
||||
|
||||
pub fn keyboardEventCreate() !*KeyboardEvent {
|
||||
var evt: ?*KeyboardEvent = undefined;
|
||||
const err = c._dom_keyboard_event_create(&evt);
|
||||
try DOMErr(err);
|
||||
return evt.?;
|
||||
}
|
||||
|
||||
pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
|
||||
c._dom_keyboard_event_destroy(evt);
|
||||
}
|
||||
|
||||
const KeyboardEventOpts = struct {
|
||||
key: []const u8,
|
||||
code: []const u8,
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
ctrl: bool = false,
|
||||
alt: bool = false,
|
||||
shift: bool = false,
|
||||
meta: bool = false,
|
||||
};
|
||||
|
||||
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
|
||||
const s = try strFromData(typ);
|
||||
const err = c._dom_keyboard_event_init(
|
||||
evt,
|
||||
s,
|
||||
opts.bubbles,
|
||||
opts.cancelable,
|
||||
null, // dom_abstract_view* ?
|
||||
try strFromData(opts.key),
|
||||
try strFromData(opts.code),
|
||||
0, // location 0 == standard
|
||||
opts.ctrl,
|
||||
opts.shift,
|
||||
opts.alt,
|
||||
opts.meta,
|
||||
false, // repease
|
||||
false, // is_composiom
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn keyboardEventGetKey(evt: *KeyboardEvent) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
_ = c._dom_keyboard_event_get_key(evt, &s);
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
// NodeType
|
||||
|
||||
pub const NodeType = enum(u4) {
|
||||
@@ -954,7 +894,7 @@ pub fn nodeListItem(nodeList: *NodeList, index: u32) !?*Node {
|
||||
const err = c._dom_nodelist_item(nodeList, index, &n);
|
||||
try DOMErr(err);
|
||||
if (n == null) return null;
|
||||
return @as(*Node, @alignCast(@ptrCast(n)));
|
||||
return @as(*Node, @ptrCast(n));
|
||||
}
|
||||
|
||||
// NodeExternal is the libdom public representation of a Node.
|
||||
@@ -1361,10 +1301,6 @@ pub inline fn nodeToElement(node: *Node) *Element {
|
||||
return @as(*Element, @ptrCast(node));
|
||||
}
|
||||
|
||||
pub inline fn nodeToHtmlElement(node: *Node) *ElementHTML {
|
||||
return @as(*ElementHTML, @alignCast(@ptrCast(node)));
|
||||
}
|
||||
|
||||
// nodeToDocument is an helper to convert a node to an document.
|
||||
pub inline fn nodeToDocument(node: *Node) *Document {
|
||||
return @as(*Document, @ptrCast(node));
|
||||
@@ -1387,7 +1323,7 @@ fn characterDataVtable(data: *CharacterData) c.dom_characterdata_vtable {
|
||||
}
|
||||
|
||||
pub inline fn characterDataToNode(cdata: *CharacterData) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(cdata)));
|
||||
return @as(*Node, @ptrCast(cdata));
|
||||
}
|
||||
|
||||
pub fn characterDataData(cdata: *CharacterData) ![]const u8 {
|
||||
@@ -1472,7 +1408,7 @@ pub const ProcessingInstruction = c.dom_processing_instruction;
|
||||
|
||||
// processingInstructionToNode is an helper to convert an ProcessingInstruction to a node.
|
||||
pub inline fn processingInstructionToNode(pi: *ProcessingInstruction) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(pi)));
|
||||
return @as(*Node, @ptrCast(pi));
|
||||
}
|
||||
|
||||
pub fn processInstructionCopy(pi: *ProcessingInstruction) !*ProcessingInstruction {
|
||||
@@ -1527,7 +1463,7 @@ pub fn attributeGetOwnerElement(a: *Attribute) !?*Element {
|
||||
|
||||
// attributeToNode is an helper to convert an attribute to a node.
|
||||
pub inline fn attributeToNode(a: *Attribute) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(a)));
|
||||
return @as(*Node, @ptrCast(a));
|
||||
}
|
||||
|
||||
// Element
|
||||
@@ -1665,7 +1601,7 @@ pub fn elementHasClass(elem: *Element, class: []const u8) !bool {
|
||||
|
||||
// elementToNode is an helper to convert an element to a node.
|
||||
pub inline fn elementToNode(e: *Element) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(e)));
|
||||
return @as(*Node, @ptrCast(e));
|
||||
}
|
||||
|
||||
// TokenList
|
||||
@@ -1749,14 +1685,14 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
|
||||
|
||||
// scriptToElt is an helper to convert an script to an element.
|
||||
pub inline fn scriptToElt(s: *Script) *Element {
|
||||
return @as(*Element, @alignCast(@ptrCast(s)));
|
||||
return @as(*Element, @ptrCast(s));
|
||||
}
|
||||
|
||||
// HTMLAnchorElement
|
||||
|
||||
// anchorToNode is an helper to convert an anchor to a node.
|
||||
pub inline fn anchorToNode(a: *Anchor) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(a)));
|
||||
return @as(*Node, @ptrCast(a));
|
||||
}
|
||||
|
||||
pub fn anchorGetTarget(a: *Anchor) ![]const u8 {
|
||||
@@ -1901,7 +1837,7 @@ pub const OptionCollection = c.dom_html_options_collection;
|
||||
pub const DocumentFragment = c.dom_document_fragment;
|
||||
|
||||
pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
}
|
||||
|
||||
pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
|
||||
@@ -1997,10 +1933,8 @@ pub inline fn domImplementationCreateDocumentType(
|
||||
return dt.?;
|
||||
}
|
||||
|
||||
pub const CreateElementFn = ?*const fn ([*c]c.dom_html_element_create_params, [*c][*c]ElementHTML) callconv(.c) c.dom_exception;
|
||||
|
||||
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML {
|
||||
const doc_html = try documentCreateDocument(title, create_element);
|
||||
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*DocumentHTML {
|
||||
const doc_html = try documentCreateDocument(title);
|
||||
const doc = documentHTMLToDocument(doc_html);
|
||||
|
||||
// add hierarchy: html, head, body.
|
||||
@@ -2013,7 +1947,7 @@ pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_ele
|
||||
if (title) |t| {
|
||||
const htitle = try documentCreateElement(doc, "title");
|
||||
const txt = try documentCreateTextNode(doc, t);
|
||||
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @alignCast(@ptrCast(txt))));
|
||||
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @ptrCast(txt)));
|
||||
_ = try nodeAppendChild(elementToNode(head), elementToNode(htitle));
|
||||
}
|
||||
|
||||
@@ -2031,7 +1965,7 @@ fn documentVtable(doc: *Document) c.dom_document_vtable {
|
||||
}
|
||||
|
||||
pub inline fn documentToNode(doc: *Document) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
}
|
||||
|
||||
pub inline fn documentGetElementById(doc: *Document, id: []const u8) !?*Element {
|
||||
@@ -2081,7 +2015,7 @@ pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void {
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub inline fn documentCreateDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML {
|
||||
pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
|
||||
var doc: ?*Document = undefined;
|
||||
const err = c.dom_implementation_create_document(
|
||||
c.DOM_IMPLEMENTATION_HTML,
|
||||
@@ -2095,9 +2029,6 @@ pub inline fn documentCreateDocument(title: ?[]const u8, create_element: CreateE
|
||||
try DOMErr(err);
|
||||
const doc_html = @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
if (title) |t| try documentHTMLSetTitle(doc_html, t);
|
||||
|
||||
doc_html.create_element_external = create_element;
|
||||
|
||||
return doc_html;
|
||||
}
|
||||
|
||||
@@ -2172,7 +2103,7 @@ pub inline fn documentImportNode(doc: *Document, node: *Node, deep: bool) !*Node
|
||||
const nodeext = toNodeExternal(Node, node);
|
||||
const err = documentVtable(doc).dom_document_import_node.?(doc, nodeext, deep, &res);
|
||||
try DOMErr(err);
|
||||
return @as(*Node, @alignCast(@ptrCast(res)));
|
||||
return @as(*Node, @ptrCast(res));
|
||||
}
|
||||
|
||||
pub inline fn documentAdoptNode(doc: *Document, node: *Node) !*Node {
|
||||
@@ -2180,7 +2111,7 @@ pub inline fn documentAdoptNode(doc: *Document, node: *Node) !*Node {
|
||||
const nodeext = toNodeExternal(Node, node);
|
||||
const err = documentVtable(doc).dom_document_adopt_node.?(doc, nodeext, &res);
|
||||
try DOMErr(err);
|
||||
return @as(*Node, @alignCast(@ptrCast(res)));
|
||||
return @as(*Node, @ptrCast(res));
|
||||
}
|
||||
|
||||
pub inline fn documentCreateAttribute(doc: *Document, name: []const u8) !*Attribute {
|
||||
@@ -2215,7 +2146,7 @@ pub const DocumentHTML = c.dom_html_document;
|
||||
|
||||
// documentHTMLToNode is an helper to convert a documentHTML to an node.
|
||||
pub inline fn documentHTMLToNode(doc: *DocumentHTML) *Node {
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
}
|
||||
|
||||
fn documentHTMLVtable(doc_html: *DocumentHTML) c.dom_html_document_vtable {
|
||||
@@ -2261,26 +2192,24 @@ fn parserErr(err: HubbubErr) ParserError!void {
|
||||
|
||||
// documentHTMLParseFromStr parses the given HTML string.
|
||||
// The caller is responsible for closing the document.
|
||||
pub fn documentHTMLParseFromStr(str: []const u8, create_element: CreateElementFn) !*DocumentHTML {
|
||||
pub fn documentHTMLParseFromStr(str: []const u8) !*DocumentHTML {
|
||||
var fbs = std.io.fixedBufferStream(str);
|
||||
return try documentHTMLParse(fbs.reader(), "UTF-8", create_element);
|
||||
return try documentHTMLParse(fbs.reader(), "UTF-8");
|
||||
}
|
||||
|
||||
pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8, create_element: CreateElementFn) !*DocumentHTML {
|
||||
pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
|
||||
var parser: ?*c.dom_hubbub_parser = undefined;
|
||||
var doc: ?*c.dom_document = undefined;
|
||||
var err: c.hubbub_error = undefined;
|
||||
var params = parseParams(enc);
|
||||
|
||||
err = c.dom_hubbub_parser_create(¶ms, &parser, &doc);
|
||||
const result = @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
result.create_element_external = create_element;
|
||||
try parserErr(err);
|
||||
defer c.dom_hubbub_parser_destroy(parser);
|
||||
|
||||
try parseData(parser.?, reader);
|
||||
|
||||
return result;
|
||||
return @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
}
|
||||
|
||||
pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment {
|
||||
@@ -2362,7 +2291,7 @@ pub inline fn documentHTMLBody(doc_html: *DocumentHTML) !?*Body {
|
||||
}
|
||||
|
||||
pub inline fn bodyToElement(body: *Body) *Element {
|
||||
return @as(*Element, @alignCast(@ptrCast(body)));
|
||||
return @as(*Element, @ptrCast(body));
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLSetBody(doc_html: *DocumentHTML, elt: ?*ElementHTML) !void {
|
||||
@@ -2401,7 +2330,7 @@ pub inline fn documentHTMLSetTitle(doc: *DocumentHTML, v: []const u8) !void {
|
||||
|
||||
pub fn documentHTMLSetCurrentScript(doc: *DocumentHTML, script: ?*Script) !void {
|
||||
var s: ?*ElementHTML = null;
|
||||
if (script != null) s = @alignCast(@ptrCast(script.?));
|
||||
if (script != null) s = @ptrCast(script.?);
|
||||
const err = documentHTMLVtable(doc).set_current_script.?(doc, s);
|
||||
try DOMErr(err);
|
||||
}
|
||||
@@ -2462,11 +2391,6 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
|
||||
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// Select
|
||||
pub fn selectGetOptions(select: *Select) !*OptionCollection {
|
||||
var collection: ?*OptionCollection = null;
|
||||
@@ -2835,7 +2759,7 @@ pub fn inputSetType(input: *Input, type_: []const u8) !void {
|
||||
}
|
||||
}
|
||||
const new_type = if (found) type_ else "text";
|
||||
try elementSetAttribute(@alignCast(@ptrCast(input)), "type", new_type);
|
||||
try elementSetAttribute(@ptrCast(input), "type", new_type);
|
||||
}
|
||||
|
||||
pub fn inputGetValue(input: *Input) ![]const u8 {
|
||||
@@ -2849,11 +2773,3 @@ pub fn inputSetValue(input: *Input, value: []const u8) !void {
|
||||
const err = c.dom_html_input_element_set_value(input, try strFromData(value));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn buttonGetType(button: *Button) ![]const u8 {
|
||||
var s_: ?*String = null;
|
||||
const err = c.dom_html_button_element_get_type(button, &s_);
|
||||
try DOMErr(err);
|
||||
const s = s_ orelse return "button";
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
@@ -80,12 +80,11 @@ pub const Page = struct {
|
||||
|
||||
microtask_node: Loop.CallbackNode,
|
||||
|
||||
keydown_event_node: parser.EventNode,
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
|
||||
// Our JavaScript context for this specific page. This is what we use to
|
||||
// execute any JavaScript
|
||||
main_context: *Env.JsContext,
|
||||
scope: *Env.Scope,
|
||||
|
||||
// List of modules currently fetched/loaded.
|
||||
module_map: std.StringHashMapUnmanaged([]const u8),
|
||||
@@ -113,18 +112,17 @@ pub const Page = struct {
|
||||
.state_pool = &browser.state_pool,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.keydown_event_node = .{ .func = keydownCallback },
|
||||
.window_clicked_event_node = .{ .func = windowClicked },
|
||||
.request_factory = browser.http_client.requestFactory(.{
|
||||
.notification = browser.notification,
|
||||
}),
|
||||
.main_context = undefined,
|
||||
.scope = undefined,
|
||||
.module_map = .empty,
|
||||
};
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
|
||||
self.scope = try session.executor.startScope(&self.window, self, self, true);
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena, self.main_context);
|
||||
try polyfill.load(self.arena, self.scope);
|
||||
|
||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
}
|
||||
@@ -166,7 +164,7 @@ pub const Page = struct {
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.main_context);
|
||||
try_catch.init(self.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
try self.session.browser.app.loop.run();
|
||||
@@ -192,12 +190,7 @@ pub const Page = struct {
|
||||
const session = self.session;
|
||||
const notification = session.browser.notification;
|
||||
|
||||
log.debug(.http, "navigate", .{
|
||||
.url = request_url,
|
||||
.method = opts.method,
|
||||
.reason = opts.reason,
|
||||
.body = opts.body != null,
|
||||
});
|
||||
log.debug(.http, "navigate", .{ .url = request_url, .reason = opts.reason });
|
||||
|
||||
// if the url is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", request_url.raw)) {
|
||||
@@ -254,8 +247,6 @@ pub const Page = struct {
|
||||
.content_type = content_type,
|
||||
.charset = mime.charset,
|
||||
.url = request_url,
|
||||
.method = opts.method,
|
||||
.reason = opts.reason,
|
||||
});
|
||||
|
||||
if (!mime.isHTML()) {
|
||||
@@ -286,8 +277,7 @@ pub const Page = struct {
|
||||
pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
const ccharset = try self.arena.dupeZ(u8, charset);
|
||||
|
||||
const Elements = @import("html/elements.zig");
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset, &Elements.createElement);
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset);
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
// inject the URL to the document including the fragment.
|
||||
@@ -315,12 +305,6 @@ pub const Page = struct {
|
||||
&self.window_clicked_event_node,
|
||||
false,
|
||||
);
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
"keydown",
|
||||
&self.keydown_event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
@@ -389,15 +373,11 @@ pub const Page = struct {
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
if (self.evalScript(&script) == false) {
|
||||
return;
|
||||
}
|
||||
self.evalScript(&script);
|
||||
}
|
||||
|
||||
for (defer_scripts.items) |*script| {
|
||||
if (self.evalScript(script) == false) {
|
||||
return;
|
||||
}
|
||||
self.evalScript(script);
|
||||
}
|
||||
// dispatch DOMContentLoaded before the transition to "complete",
|
||||
// at the point where all subresources apart from async script elements
|
||||
@@ -407,9 +387,7 @@ pub const Page = struct {
|
||||
|
||||
// eval async scripts.
|
||||
for (async_scripts.items) |*script| {
|
||||
if (self.evalScript(script) == false) {
|
||||
return;
|
||||
}
|
||||
self.evalScript(script);
|
||||
}
|
||||
|
||||
try HTMLDocument.documentIsComplete(html_doc, self);
|
||||
@@ -426,13 +404,10 @@ pub const Page = struct {
|
||||
);
|
||||
}
|
||||
|
||||
fn evalScript(self: *Page, script: *const Script) bool {
|
||||
self.tryEvalScript(script) catch |err| switch (err) {
|
||||
error.JsErr => {}, // already been logged with detail
|
||||
error.Terminated => return false,
|
||||
else => log.err(.js, "eval script error", .{ .err = err, .src = script.src }),
|
||||
fn evalScript(self: *Page, script: *const Script) void {
|
||||
self.tryEvalScript(script) catch |err| {
|
||||
log.err(.js, "eval script error", .{ .err = err, .src = script.src });
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
@@ -446,26 +421,29 @@ pub const Page = struct {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
var script_source: ?[]const u8 = null;
|
||||
if (script.src) |src| {
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
script_source = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
const src = script.src orelse {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
script_source = try parser.nodeTextContent(parser.elementToNode(script.element));
|
||||
}
|
||||
if (try parser.nodeTextContent(parser.elementToNode(script.element))) |text| {
|
||||
try script.eval(self, text);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (script_source) |ss| {
|
||||
try script.eval(self, ss);
|
||||
}
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const body = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
|
||||
script.eval(self, body) catch |err| switch (err) {
|
||||
error.JsErr => {}, // nothing to do here.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
@@ -594,14 +572,14 @@ pub const Page = struct {
|
||||
},
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||
const input_type = (try parser.elementGetAttribute(element, "type")) orelse return;
|
||||
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
|
||||
return self.elementSubmitForm(element);
|
||||
}
|
||||
},
|
||||
.button => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const button_type = try parser.buttonGetType(@ptrCast(element));
|
||||
const button_type = (try parser.elementGetAttribute(element, "type")) orelse return;
|
||||
if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
|
||||
return self.elementSubmitForm(element);
|
||||
}
|
||||
@@ -615,111 +593,18 @@ pub const Page = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub const KeyboardEvent = struct {
|
||||
type: Type,
|
||||
key: []const u8,
|
||||
code: []const u8,
|
||||
alt: bool,
|
||||
ctrl: bool,
|
||||
meta: bool,
|
||||
shift: bool,
|
||||
|
||||
const Type = enum {
|
||||
keydown,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
|
||||
if (kbe.type != .keydown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Document = @import("dom/document.zig").Document;
|
||||
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
|
||||
|
||||
const event = try parser.keyboardEventCreate();
|
||||
defer parser.keyboardEventDestroy(event);
|
||||
try parser.keyboardEventInit(event, "keydown", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.key = kbe.key,
|
||||
.code = kbe.code,
|
||||
.alt = kbe.alt,
|
||||
.ctrl = kbe.ctrl,
|
||||
.meta = kbe.meta,
|
||||
.shift = kbe.shift,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
}
|
||||
|
||||
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const self: *Page = @fieldParentPtr("keydown_event_node", node);
|
||||
self._keydownCallback(event) catch |err| {
|
||||
log.err(.browser, "keydown handler error", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn _keydownCallback(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
const node = parser.eventTargetToNode(target);
|
||||
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||
|
||||
const kbe: *parser.KeyboardEvent = @ptrCast(event);
|
||||
var new_key = try parser.keyboardEventGetKey(kbe);
|
||||
if (std.mem.eql(u8, new_key, "Dead")) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||
if (std.mem.eql(u8, input_type, "text")) {
|
||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||
const form = (try self.formForElement(element)) orelse return;
|
||||
return self.submitForm(@ptrCast(form), null);
|
||||
}
|
||||
|
||||
const value = try parser.inputGetValue(@ptrCast(element));
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||
}
|
||||
},
|
||||
.textarea => {
|
||||
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||
new_key = "\n";
|
||||
}
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree,
|
||||
// which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// The page.arena is safe to use here, but the transfer_arena exists
|
||||
// specifically for this type of lifetime.
|
||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
|
||||
log.debug(.browser, "delayed navigation", .{
|
||||
.url = url,
|
||||
.reason = opts.reason,
|
||||
});
|
||||
self.delayed_navigation = true;
|
||||
|
||||
const session = self.session;
|
||||
const arena = session.transfer_arena;
|
||||
const arena = self.session.transfer_arena;
|
||||
const navi = try arena.create(DelayedNavigation);
|
||||
navi.* = .{
|
||||
.opts = opts,
|
||||
.session = session,
|
||||
.url = try self.url.resolve(arena, url),
|
||||
.session = self.session,
|
||||
.url = try arena.dupe(u8, url),
|
||||
};
|
||||
|
||||
// In v8, this throws an exception which JS code cannot catch.
|
||||
session.executor.terminateExecution();
|
||||
_ = try self.loop.timeout(0, &navi.navigate_node);
|
||||
}
|
||||
|
||||
@@ -748,13 +633,13 @@ pub const Page = struct {
|
||||
const transfer_arena = self.session.transfer_arena;
|
||||
var form_data = try FormData.fromForm(form, submitter, self);
|
||||
|
||||
const encoding = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "enctype");
|
||||
const encoding = try parser.elementGetAttribute(@ptrCast(form), "enctype");
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try form_data.write(encoding, buf.writer(transfer_arena));
|
||||
|
||||
const method = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "action") orelse self.url.raw;
|
||||
const method = try parser.elementGetAttribute(@ptrCast(form), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@ptrCast(form), "action") orelse self.url.raw;
|
||||
|
||||
var opts = NavigateOpts{
|
||||
.reason = .form,
|
||||
@@ -765,6 +650,7 @@ pub const Page = struct {
|
||||
} else {
|
||||
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
||||
}
|
||||
|
||||
try self.navigateFromWebAPI(action, opts);
|
||||
}
|
||||
|
||||
@@ -799,89 +685,22 @@ pub const Page = struct {
|
||||
|
||||
pub fn stackTrace(self: *Page) !?[]const u8 {
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
return self.main_context.stackTrace();
|
||||
return self.scope.stackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const DelayedNavigation = struct {
|
||||
url: URL,
|
||||
url: []const u8,
|
||||
session: *Session,
|
||||
opts: NavigateOpts,
|
||||
initial: bool = true,
|
||||
navigate_node: Loop.CallbackNode = .{ .func = delayNavigate },
|
||||
|
||||
// Navigation is blocking, which is problem because it can seize up
|
||||
// the loop and deadlock. We can only safely try to navigate to a
|
||||
// new page when we're sure there's at least 1 free slot in the
|
||||
// http client. We handle this in two phases:
|
||||
//
|
||||
// In the first phase, when self.initial == true, we'll shutdown the page
|
||||
// and create a new one. The shutdown is important, because it resets the
|
||||
// loop ctx_id and removes the JsContext. Removing the context calls our XHR
|
||||
// destructors which aborts requests. This is necessary to make sure our
|
||||
// [blocking] navigate won't block.
|
||||
//
|
||||
// In the 2nd phase, we wait until there's a free http slot so that our
|
||||
// navigate definetly won't block (which could deadlock the system if there
|
||||
// are still pending async requests, which we've seen happen, even after
|
||||
// an abort).
|
||||
fn delayNavigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
_ = repeat_delay;
|
||||
const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node);
|
||||
|
||||
const session = self.session;
|
||||
const initial = self.initial;
|
||||
|
||||
if (initial) {
|
||||
// Prior to schedule this task, we terminated excution to stop
|
||||
// the running script. If we don't resume it before doing a shutdown
|
||||
// we'll get an error.
|
||||
session.executor.resumeExecution();
|
||||
|
||||
session.removePage();
|
||||
_ = session.createPage() catch |err| {
|
||||
log.err(.browser, "delayed navigation page error", .{
|
||||
.err = err,
|
||||
.url = self.url,
|
||||
});
|
||||
return;
|
||||
};
|
||||
self.initial = false;
|
||||
}
|
||||
|
||||
if (session.browser.http_client.freeSlotCount() == 0) {
|
||||
log.debug(.browser, "delayed navigate waiting", .{});
|
||||
const delay = 0 * std.time.ns_per_ms;
|
||||
|
||||
// If this isn't the initial check, we can safely re-use the timer
|
||||
// to check again.
|
||||
if (initial == false) {
|
||||
repeat_delay.* = delay;
|
||||
return;
|
||||
}
|
||||
|
||||
// However, if this _is_ the initial check, we called
|
||||
// session.removePage above, and that reset the loop ctx_id.
|
||||
// We can't re-use this timer, because it has the previous ctx_id.
|
||||
// We can create a new timeout though, and that'll get the new ctx_id.
|
||||
//
|
||||
// Page has to be not-null here because we called createPage above.
|
||||
_ = session.page.?.loop.timeout(delay, &self.navigate_node) catch |err| {
|
||||
log.err(.browser, "delayed navigation loop err", .{ .err = err });
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const page = session.currentPage() orelse return;
|
||||
defer if (!page.delayed_navigation) {
|
||||
// If, while loading the page, we intend to navigate to another
|
||||
// page, then we need to keep the transfer_arena around, as this
|
||||
// sub-navigation is probably using it.
|
||||
_ = session.browser.transfer_arena.reset(.{ .retain_with_limit = 64 * 1024 });
|
||||
};
|
||||
|
||||
return page.navigate(self.url, self.opts) catch |err| {
|
||||
self.session.pageNavigate(self.url, self.opts) catch |err| {
|
||||
log.err(.browser, "delayed navigation error", .{ .err = err, .url = self.url });
|
||||
};
|
||||
}
|
||||
@@ -983,14 +802,14 @@ const Script = struct {
|
||||
|
||||
fn eval(self: *const Script, page: *Page, body: []const u8) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
try_catch.init(page.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const src = self.src orelse "inline";
|
||||
_ = switch (self.kind) {
|
||||
.javascript => page.main_context.exec(body, src),
|
||||
.javascript => page.scope.exec(body, src),
|
||||
.module => blk: {
|
||||
switch (try page.main_context.module(body, src)) {
|
||||
switch (try page.scope.module(body, src)) {
|
||||
.value => |v| break :blk v,
|
||||
.exception => |e| {
|
||||
log.warn(.user_script, "eval module", .{
|
||||
@@ -1002,17 +821,9 @@ const Script = struct {
|
||||
}
|
||||
},
|
||||
} catch {
|
||||
if (page.delayed_navigation) {
|
||||
return error.Terminated;
|
||||
}
|
||||
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.src = src,
|
||||
.err = msg,
|
||||
});
|
||||
log.warn(.user_script, "eval script", .{ .src = src, .err = msg });
|
||||
}
|
||||
|
||||
try self.executeCallback("onerror", page);
|
||||
return error.JsErr;
|
||||
};
|
||||
@@ -1024,9 +835,9 @@ const Script = struct {
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
try_catch.init(page.scope);
|
||||
defer try_catch.deinit();
|
||||
_ = page.main_context.exec(str, typ) catch {
|
||||
_ = page.scope.exec(str, typ) catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.src = self.src,
|
||||
@@ -1085,15 +896,10 @@ fn timestamp() u32 {
|
||||
// immediately.
|
||||
pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) callconv(.C) void {
|
||||
const self: *Page = @alignCast(@ptrCast(ctx.?));
|
||||
if (self.delayed_navigation) {
|
||||
// if we're planning on navigating to another page, don't run this script
|
||||
return;
|
||||
}
|
||||
|
||||
var script = Script.init(element.?, self) catch |err| {
|
||||
log.warn(.browser, "script added init error", .{ .err = err });
|
||||
return;
|
||||
} orelse return;
|
||||
|
||||
_ = self.evalScript(&script);
|
||||
self.evalScript(&script);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ test "Browser.fetch" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.page.scope);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
|
||||
@@ -30,13 +30,13 @@ const modules = [_]struct {
|
||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||
};
|
||||
|
||||
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
try_catch.init(scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
for (modules) |m| {
|
||||
_ = js_context.exec(m.source, m.name) catch |err| {
|
||||
_ = scope.exec(m.source, m.name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg });
|
||||
|
||||
@@ -22,7 +22,6 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const URL = @import("../url.zig").URL;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||
|
||||
@@ -73,7 +72,7 @@ pub const Session = struct {
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
self.removePage() catch {};
|
||||
}
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit();
|
||||
@@ -85,14 +84,14 @@ pub const Session = struct {
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
std.debug.assert(self.page == null);
|
||||
|
||||
// Start netsurf memory arena.
|
||||
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
||||
try parser.init();
|
||||
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
|
||||
|
||||
// Start netsurf memory arena.
|
||||
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
||||
try parser.init(page_arena.allocator());
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_arena.allocator(), self);
|
||||
@@ -105,7 +104,7 @@ pub const Session = struct {
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
pub fn removePage(self: *Session) !void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
@@ -116,11 +115,11 @@ pub const Session = struct {
|
||||
// phase. It's important that we clean these up, as they're holding onto
|
||||
// limited resources (like our fixed-sized http state pool).
|
||||
//
|
||||
// First thing we do, is removeJsContext() which will execute the destructor
|
||||
// First thing we do, is endScope() which will execute the destructor
|
||||
// of any type that registered a destructor (e.g. XMLHttpRequest).
|
||||
// This will shutdown any pending sockets, which begins our cleaning
|
||||
// processed
|
||||
self.executor.removeJsContext();
|
||||
self.executor.endScope();
|
||||
|
||||
// Second thing we do is reset the loop. This increments the loop ctx_id
|
||||
// so that any "stale" timeouts we process will get ignored. We need to
|
||||
@@ -128,6 +127,12 @@ pub const Session = struct {
|
||||
// window.setTimeout and running microtasks should be ignored
|
||||
self.browser.app.loop.reset();
|
||||
|
||||
// Finally, we run the loop. Because of the reset just above, this will
|
||||
// ignore any timeouts. And, because of the endScope about this, it
|
||||
// should ensure that the http requests detect the shutdown socket and
|
||||
// release their resources.
|
||||
try self.browser.app.loop.run();
|
||||
|
||||
self.page = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
@@ -139,4 +144,28 @@ pub const Session = struct {
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub fn pageNavigate(self: *Session, url_string: []const u8, opts: NavigateOpts) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
defer if (self.page) |*p| {
|
||||
if (!p.delayed_navigation) {
|
||||
// If, while loading the page, we intend to navigate to another
|
||||
// page, then we need to keep the transfer_arena around, as this
|
||||
// sub-navigation is probably using it.
|
||||
_ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 64 * 1024 });
|
||||
}
|
||||
};
|
||||
|
||||
// it's safe to use the transfer arena here, because the page will
|
||||
// eventually clone the URL using its own page_arena (after it gets
|
||||
// the final URL, possibly following redirects)
|
||||
const url = try self.page.?.url.resolve(self.transfer_arena, url_string);
|
||||
|
||||
try self.removePage();
|
||||
var page = try self.createPage();
|
||||
return page.navigate(url, opts);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ pub const Interfaces = .{
|
||||
// allocatorate data, I should be able to retrieve the scheme + the following `:`
|
||||
// from rawuri.
|
||||
//
|
||||
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
|
||||
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
|
||||
// parser including the characters we want for the web API.
|
||||
pub const URL = struct {
|
||||
uri: std.Uri,
|
||||
|
||||
@@ -137,7 +137,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)));
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const tpe = try parser.inputGetType(@ptrCast(element));
|
||||
const tpe = try parser.elementGetAttribute(element, "type") orelse "";
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image")) {
|
||||
if (submitter_name_) |submitter_name| {
|
||||
if (std.mem.eql(u8, submitter_name, name)) {
|
||||
@@ -162,7 +162,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
}
|
||||
submitter_included = true;
|
||||
}
|
||||
const value = try parser.inputGetValue(@ptrCast(element));
|
||||
const value = (try parser.elementGetAttribute(element, "value")) orelse "";
|
||||
try entries.appendOwned(arena, name, value);
|
||||
},
|
||||
.select => {
|
||||
@@ -189,11 +189,11 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
}
|
||||
|
||||
if (submitter_included == false) {
|
||||
if (submitter_name_) |submitter_name| {
|
||||
if (submitter_) |submitter| {
|
||||
// this can happen if the submitter is outside the form, but associated
|
||||
// with the form via a form=ID attribute
|
||||
const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
|
||||
try entries.appendOwned(arena, submitter_name, value);
|
||||
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse "";
|
||||
try entries.appendOwned(arena, submitter_name_.?, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
|
||||
if (is_multiple == false) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(selected_index));
|
||||
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(option)), "disabled") != null) {
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
return;
|
||||
}
|
||||
const value = try parser.optionGetValue(option);
|
||||
@@ -228,7 +228,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
|
||||
// we can go directly to the first one
|
||||
for (@intCast(selected_index)..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(option)), "disabled") != null) {
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 {
|
||||
switch (tag) {
|
||||
.button => return name,
|
||||
.input => {
|
||||
const tpe = try parser.inputGetType(@ptrCast(element));
|
||||
const tpe = (try parser.elementGetAttribute(element, "type")) orelse "";
|
||||
// only an image type can be a sumbitter
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
|
||||
return name;
|
||||
|
||||
@@ -338,20 +338,9 @@ pub const XMLHttpRequest = struct {
|
||||
// dispatch request event.
|
||||
// errors are logged only.
|
||||
fn dispatchEvt(self: *XMLHttpRequest, typ: []const u8) void {
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
log.debug(.script_event, "dispatch event", .{ .type = typ, .source = "xhr" });
|
||||
self._dispatchEvt(typ) catch |err| {
|
||||
log.err(.app, "dispatch event error", .{
|
||||
.err = err,
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
log.err(.app, "dispatch event error", .{ .err = err, .type = typ, .source = "xhr" });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -369,20 +358,9 @@ pub const XMLHttpRequest = struct {
|
||||
typ: []const u8,
|
||||
opts: ProgressEvent.EventInit,
|
||||
) void {
|
||||
log.debug(.script_event, "dispatch progress event", .{
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
log.debug(.script_event, "dispatch progress event", .{ .type = typ, .source = "xhr" });
|
||||
self._dispatchProgressEvent(typ, opts) catch |err| {
|
||||
log.err(.app, "dispatch progress event error", .{
|
||||
.err = err,
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
log.err(.app, "dispatch progress event error", .{ .err = err, .type = typ, .source = "xhr" });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -756,8 +734,7 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
|
||||
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
|
||||
const Elements = @import("../html/elements.zig");
|
||||
const doc = parser.documentHTMLParse(fbs.reader(), ccharset, &Elements.createElement) catch {
|
||||
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
|
||||
self.response_obj = .{ .Failure = {} };
|
||||
return;
|
||||
};
|
||||
|
||||
52
src/cdp/cbor/cbor.zig
Normal file
52
src/cdp/cbor/cbor.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2023-2024 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/>.
|
||||
|
||||
pub const jsonToCbor = @import("json_to_cbor.zig").jsonToCbor;
|
||||
pub const cborToJson = @import("cbor_to_json.zig").cborToJson;
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "cbor" {
|
||||
try testCbor("{\"x\":null}");
|
||||
try testCbor("{\"x\":true}");
|
||||
try testCbor("{\"x\":false}");
|
||||
try testCbor("{\"x\":0}");
|
||||
try testCbor("{\"x\":1}");
|
||||
try testCbor("{\"x\":-1}");
|
||||
try testCbor("{\"x\":4832839283}");
|
||||
try testCbor("{\"x\":-998128383}");
|
||||
try testCbor("{\"x\":48328.39283}");
|
||||
try testCbor("{\"x\":-9981.28383}");
|
||||
try testCbor("{\"x\":\"\"}");
|
||||
try testCbor("{\"x\":\"over 9000!\"}");
|
||||
|
||||
try testCbor("{\"x\":[]}");
|
||||
try testCbor("{\"x\":{}}");
|
||||
}
|
||||
|
||||
fn testCbor(json: []const u8) !void {
|
||||
const std = @import("std");
|
||||
|
||||
defer testing.reset();
|
||||
const encoded = try jsonToCbor(testing.arena_allocator, json);
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try cborToJson(encoded, arr.writer(testing.arena_allocator));
|
||||
|
||||
try testing.expectEqual(json, arr.items);
|
||||
}
|
||||
252
src/cdp/cbor/cbor_to_json.zig
Normal file
252
src/cdp/cbor/cbor_to_json.zig
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright (C) 2023-2024 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 Allocator = std.mem.Allocator;
|
||||
|
||||
const Error = error{
|
||||
EOSReadingFloat,
|
||||
UnknownTag,
|
||||
EOSReadingArray,
|
||||
UnterminatedArray,
|
||||
EOSReadingMap,
|
||||
UnterminatedMap,
|
||||
EOSReadingLength,
|
||||
InvalidLength,
|
||||
MissingData,
|
||||
EOSExpectedString,
|
||||
ExpectedString,
|
||||
OutOfMemory,
|
||||
EmbeddedDataIsShort,
|
||||
InvalidEmbeddedDataEnvelope,
|
||||
};
|
||||
|
||||
pub fn cborToJson(input: []const u8, writer: anytype) !void {
|
||||
if (input.len < 7) {
|
||||
return error.InvalidCBORMessage;
|
||||
}
|
||||
|
||||
var data = input;
|
||||
while (data.len > 0) {
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeValue(data: []const u8, writer: anytype) Error![]const u8 {
|
||||
switch (data[0]) {
|
||||
0xf4 => {
|
||||
try writer.writeAll("false");
|
||||
return data[1..];
|
||||
},
|
||||
0xf5 => {
|
||||
try writer.writeAll("true");
|
||||
return data[1..];
|
||||
},
|
||||
0xf6, 0xf7 => { // 0xf7 is undefined
|
||||
try writer.writeAll("null");
|
||||
return data[1..];
|
||||
},
|
||||
0x9f => return writeInfiniteArray(data[1..], writer),
|
||||
0xbf => return writeInfiniteMap(data[1..], writer),
|
||||
0xd8 => {
|
||||
// This is major type 6, which is generic tagged data. We only
|
||||
// support 1 tag: embedded cbor data.
|
||||
if (data.len < 7) {
|
||||
return error.EmbeddedDataIsShort;
|
||||
}
|
||||
if (data[1] != 0x18 or data[2] != 0x5a) {
|
||||
return error.InvalidEmbeddedDataEnvelope;
|
||||
}
|
||||
// skip the length, we have the full paylaod
|
||||
return writeValue(data[7..], writer);
|
||||
},
|
||||
0xf9 => { // f16
|
||||
if (data.len < 3) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f16, @bitCast(std.mem.readInt(u16, data[1..3], .big)))});
|
||||
return data[3..];
|
||||
},
|
||||
0xfa => { // f32
|
||||
if (data.len < 5) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f32, @bitCast(std.mem.readInt(u32, data[1..5], .big)))});
|
||||
return data[5..];
|
||||
},
|
||||
0xfb => { // f64
|
||||
if (data.len < 9) {
|
||||
return error.EOSReadingFloat;
|
||||
}
|
||||
try writer.print("{d}", .{@as(f64, @bitCast(std.mem.readInt(u64, data[1..9], .big)))});
|
||||
return data[9..];
|
||||
},
|
||||
else => |b| {
|
||||
const major_type = b >> 5;
|
||||
switch (major_type) {
|
||||
0 => {
|
||||
const rest, const length = try parseLength(data);
|
||||
try writer.print("{d}", .{length});
|
||||
return rest;
|
||||
},
|
||||
1 => {
|
||||
const rest, const length = try parseLength(data);
|
||||
try writer.print("{d}", .{-@as(i64, @intCast(length)) - 1});
|
||||
return rest;
|
||||
},
|
||||
2 => {
|
||||
const rest, const str = try parseString(data);
|
||||
try writer.writeByte('"');
|
||||
try std.base64.standard.Encoder.encodeWriter(writer, str);
|
||||
try writer.writeByte('"');
|
||||
return rest;
|
||||
},
|
||||
3 => {
|
||||
const rest, const str = try parseString(data);
|
||||
try std.json.encodeJsonString(str, .{}, writer);
|
||||
return rest;
|
||||
},
|
||||
// 4 => unreachable, // fixed-length array
|
||||
// 5 => unreachable, // fixed-length map
|
||||
else => return error.UnknownTag,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// We expect every array from V8 to be an infinite-length array. That it, it
|
||||
// starts with the special tag: (4<<5) | 31 which an "array" with infinite
|
||||
// length.
|
||||
// Of course, it isn't infite, the end of the array happens when we hit a break
|
||||
// code which is FF (7 << 5) | 31
|
||||
fn writeInfiniteArray(d: []const u8, writer: anytype) ![]const u8 {
|
||||
if (d.len == 0) {
|
||||
return error.EOSReadingArray;
|
||||
}
|
||||
if (d[0] == 255) {
|
||||
try writer.writeAll("[]");
|
||||
return d[1..];
|
||||
}
|
||||
|
||||
try writer.writeByte('[');
|
||||
var data = try writeValue(d, writer);
|
||||
while (data.len > 0) {
|
||||
if (data[0] == 255) {
|
||||
try writer.writeByte(']');
|
||||
return data[1..];
|
||||
}
|
||||
try writer.writeByte(',');
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
|
||||
// Reaching the end of the input is a mistake, should have reached the break
|
||||
// code
|
||||
return error.UnterminatedArray;
|
||||
}
|
||||
|
||||
// We expect every map from V8 to be an infinite-length map. That it, it
|
||||
// starts with the special tag: (5<<5) | 31 which an "map" with infinite
|
||||
// length.
|
||||
// Of course, it isn't infite, the end of the map happens when we hit a break
|
||||
// code which is FF (7 << 5) | 31
|
||||
fn writeInfiniteMap(d: []const u8, writer: anytype) ![]const u8 {
|
||||
if (d.len == 0) {
|
||||
return error.EOSReadingMap;
|
||||
}
|
||||
if (d[0] == 255) {
|
||||
try writer.writeAll("{}");
|
||||
return d[1..];
|
||||
}
|
||||
|
||||
try writer.writeByte('{');
|
||||
|
||||
var data = blk: {
|
||||
const data, const field = try maybeParseString(d);
|
||||
try std.json.encodeJsonString(field, .{}, writer);
|
||||
try writer.writeByte(':');
|
||||
break :blk try writeValue(data, writer);
|
||||
};
|
||||
|
||||
while (data.len > 0) {
|
||||
if (data[0] == 255) {
|
||||
try writer.writeByte('}');
|
||||
return data[1..];
|
||||
}
|
||||
try writer.writeByte(',');
|
||||
data, const field = try maybeParseString(data);
|
||||
try std.json.encodeJsonString(field, .{}, writer);
|
||||
try writer.writeByte(':');
|
||||
data = try writeValue(data, writer);
|
||||
}
|
||||
|
||||
// Reaching the end of the input is a mistake, should have reached the break
|
||||
// code
|
||||
return error.UnterminatedMap;
|
||||
}
|
||||
|
||||
fn parseLength(data: []const u8) !struct { []const u8, usize } {
|
||||
std.debug.assert(data.len > 0);
|
||||
switch (data[0] & 0b11111) {
|
||||
0...23 => |n| return .{ data[1..], n },
|
||||
24 => {
|
||||
if (data.len == 1) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[2..], @intCast(data[1]) };
|
||||
},
|
||||
25 => {
|
||||
if (data.len < 3) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[3..], @intCast(std.mem.readInt(u16, data[1..3], .big)) };
|
||||
},
|
||||
26 => {
|
||||
if (data.len < 5) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[5..], @intCast(std.mem.readInt(u32, data[1..5], .big)) };
|
||||
},
|
||||
27 => {
|
||||
if (data.len < 9) {
|
||||
return error.EOSReadingLength;
|
||||
}
|
||||
return .{ data[9..], @intCast(std.mem.readInt(u64, data[1..9], .big)) };
|
||||
},
|
||||
else => return error.InvalidLength,
|
||||
}
|
||||
}
|
||||
|
||||
fn parseString(data: []const u8) !struct { []const u8, []const u8 } {
|
||||
const rest, const length = try parseLength(data);
|
||||
if (rest.len < length) {
|
||||
return error.MissingData;
|
||||
}
|
||||
return .{ rest[length..], rest[0..length] };
|
||||
}
|
||||
|
||||
fn maybeParseString(data: []const u8) !struct { []const u8, []const u8 } {
|
||||
if (data.len == 0) {
|
||||
return error.EOSExpectedString;
|
||||
}
|
||||
const b = data[0];
|
||||
if (b >> 5 != 3) {
|
||||
return error.ExpectedString;
|
||||
}
|
||||
return parseString(data);
|
||||
}
|
||||
173
src/cdp/cbor/json_to_cbor.zig
Normal file
173
src/cdp/cbor/json_to_cbor.zig
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (C) 2023-2024 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 json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Error = error{
|
||||
InvalidJson,
|
||||
OutOfMemory,
|
||||
SyntaxError,
|
||||
UnexpectedEndOfInput,
|
||||
ValueTooLong,
|
||||
};
|
||||
|
||||
pub fn jsonToCbor(arena: Allocator, input: []const u8) ![]const u8 {
|
||||
var scanner = json.Scanner.initCompleteInput(arena, input);
|
||||
defer scanner.deinit();
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try writeNext(arena, &arr, &scanner);
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
fn writeNext(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) Error!void {
|
||||
const token = scanner.nextAlloc(arena, .alloc_if_needed) catch return error.InvalidJson;
|
||||
return writeToken(arena, arr, scanner, token);
|
||||
}
|
||||
|
||||
fn writeToken(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner, token: json.Token) Error!void {
|
||||
switch (token) {
|
||||
.object_begin => return writeObject(arena, arr, scanner),
|
||||
.array_begin => return writeArray(arena, arr, scanner),
|
||||
.true => return arr.append(arena, 7 << 5 | 21),
|
||||
.false => return arr.append(arena, 7 << 5 | 20),
|
||||
.null => return arr.append(arena, 7 << 5 | 22),
|
||||
.allocated_string, .string => |key| return writeString(arena, arr, key),
|
||||
.allocated_number, .number => |s| {
|
||||
if (json.isNumberFormattedLikeAnInteger(s)) {
|
||||
return writeInteger(arena, arr, s);
|
||||
}
|
||||
const f = std.fmt.parseFloat(f64, s) catch unreachable;
|
||||
return writeHeader(arena, arr, 7, @intCast(@as(u64, @bitCast(f))));
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn writeObject(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) !void {
|
||||
const envelope = try startEmbeddedMessage(arena, arr);
|
||||
|
||||
// MajorType 5 (map) | 5-byte infinite length
|
||||
try arr.append(arena, 5 << 5 | 31);
|
||||
|
||||
while (true) {
|
||||
switch (try scanner.nextAlloc(arena, .alloc_if_needed)) {
|
||||
.allocated_string, .string => |key| {
|
||||
try writeString(arena, arr, key);
|
||||
try writeNext(arena, arr, scanner);
|
||||
},
|
||||
.object_end => {
|
||||
// MajorType 7 (break) | 5-byte infinite length
|
||||
try arr.append(arena, 7 << 5 | 31);
|
||||
return finalizeEmbeddedMessage(arr, envelope);
|
||||
},
|
||||
else => return error.InvalidJson,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeArray(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), scanner: *json.Scanner) !void {
|
||||
const envelope = try startEmbeddedMessage(arena, arr);
|
||||
|
||||
// MajorType 4 (array) | 5-byte infinite length
|
||||
try arr.append(arena, 4 << 5 | 31);
|
||||
while (true) {
|
||||
const token = scanner.nextAlloc(arena, .alloc_if_needed) catch return error.InvalidJson;
|
||||
switch (token) {
|
||||
.array_end => {
|
||||
// MajorType 7 (break) | 5-byte infinite length
|
||||
try arr.append(arena, 7 << 5 | 31);
|
||||
return finalizeEmbeddedMessage(arr, envelope);
|
||||
},
|
||||
else => try writeToken(arena, arr, scanner, token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeString(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), value: []const u8) !void {
|
||||
try writeHeader(arena, arr, 3, value.len);
|
||||
return arr.appendSlice(arena, value);
|
||||
}
|
||||
|
||||
fn writeInteger(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
|
||||
const n = std.fmt.parseInt(i64, s, 10) catch {
|
||||
return error.InvalidJson;
|
||||
};
|
||||
if (n >= 0) {
|
||||
return writeHeader(arena, arr, 0, @intCast(n));
|
||||
}
|
||||
return writeHeader(arena, arr, 1, @intCast(-1 - n));
|
||||
}
|
||||
|
||||
fn writeHeader(arena: Allocator, arr: *std.ArrayListUnmanaged(u8), comptime typ: u8, count: usize) !void {
|
||||
switch (count) {
|
||||
0...23 => try arr.append(arena, typ << 5 | @as(u8, @intCast(count))),
|
||||
24...255 => {
|
||||
try arr.ensureUnusedCapacity(arena, 2);
|
||||
arr.appendAssumeCapacity(typ << 5 | 24);
|
||||
arr.appendAssumeCapacity(@intCast(count));
|
||||
},
|
||||
256...65535 => {
|
||||
try arr.ensureUnusedCapacity(arena, 3);
|
||||
arr.appendAssumeCapacity(typ << 5 | 25);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
65536...4294967295 => {
|
||||
try arr.ensureUnusedCapacity(arena, 5);
|
||||
arr.appendAssumeCapacity(typ << 5 | 26);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 24) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 16) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
else => {
|
||||
try arr.ensureUnusedCapacity(arena, 9);
|
||||
arr.appendAssumeCapacity(typ << 5 | 27);
|
||||
arr.appendAssumeCapacity(@intCast((count >> 56) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 48) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 40) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 32) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 24) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 16) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast((count >> 8) & 0xff));
|
||||
arr.appendAssumeCapacity(@intCast(count & 0xff));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// I don't know why, but V8 expects any array or map (including the outer-most
|
||||
// object), to be encoded as embedded cbor data. This is CBOR that contains CBOR.
|
||||
// I feel that it's fine that it supports it, but why _require_ it? Seems like
|
||||
// a waste of 7 bytes.
|
||||
fn startEmbeddedMessage(arena: Allocator, arr: *std.ArrayListUnmanaged(u8)) !usize {
|
||||
try arr.appendSlice(arena, &.{ 0xd8, 0x18, 0x5a, 0, 0, 0, 0 });
|
||||
return arr.items.len;
|
||||
}
|
||||
|
||||
fn finalizeEmbeddedMessage(arr: *std.ArrayListUnmanaged(u8), pos: usize) !void {
|
||||
var items = arr.items;
|
||||
const length = items.len - pos;
|
||||
items[pos - 4] = @intCast((length >> 24) & 0xff);
|
||||
items[pos - 3] = @intCast((length >> 16) & 0xff);
|
||||
items[pos - 2] = @intCast((length >> 8) & 0xff);
|
||||
items[pos - 1] = @intCast(length & 0xff);
|
||||
}
|
||||
@@ -17,10 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const cbor = @import("cbor/cbor.zig");
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
@@ -412,13 +414,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
}
|
||||
|
||||
pub fn networkEnable(self: *Self) !void {
|
||||
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
|
||||
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
|
||||
try self.cdp.browser.notification.register(.http_request_complete, self, onHttpRequestComplete);
|
||||
}
|
||||
|
||||
pub fn networkDisable(self: *Self) void {
|
||||
self.cdp.browser.notification.unregister(.http_request_fail, self);
|
||||
self.cdp.browser.notification.unregister(.http_request_start, self);
|
||||
self.cdp.browser.notification.unregister(.http_request_complete, self);
|
||||
}
|
||||
@@ -450,12 +450,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
return @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, data);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestComplete(ctx: *anyopaque, data: *const Notification.RequestComplete) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
@@ -466,31 +460,21 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *const Self, msg: []const u8) void {
|
||||
self.inspector.send(msg);
|
||||
pub fn callInspector(self: *const Self, arena: Allocator, input: []const u8) !void {
|
||||
const encoded = try cbor.jsonToCbor(arena, input);
|
||||
try self.inspector.send(encoded);
|
||||
// force running micro tasks after send input to the inspector.
|
||||
self.cdp.browser.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, str: Env.Inspector.StringView) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), str) catch |err| {
|
||||
log.err(.cdp, "send inspector response", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
|
||||
if (log.enabled(.cdp, .debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":"));
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.err(.cdp, "invalid inspector event", .{ .msg = msg });
|
||||
return;
|
||||
};
|
||||
const method = msg[10..method_end];
|
||||
log.debug(.cdp, "inspector event", .{ .method = method });
|
||||
}
|
||||
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, str: Env.Inspector.StringView) void {
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), str) catch |err| {
|
||||
log.err(.cdp, "send inspector event", .{ .err = err });
|
||||
};
|
||||
}
|
||||
@@ -498,7 +482,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||
// session_id onto it. Second, we're much more client/websocket aware than
|
||||
// we should be.
|
||||
fn sendInspectorMessage(self: *Self, msg: []const u8) !void {
|
||||
fn sendInspectorMessage(self: *Self, str: Env.Inspector.StringView) !void {
|
||||
const session_id = self.session_id orelse {
|
||||
// We no longer have an active session. What should we do
|
||||
// in this case?
|
||||
@@ -509,27 +493,26 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
var arena = std.heap.ArenaAllocator.init(cdp.allocator);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const field = ",\"sessionId\":\"";
|
||||
|
||||
// + 1 for the closing quote after the session id
|
||||
// + 10 for the max websocket header
|
||||
const message_len = msg.len + session_id.len + 1 + field.len + 10;
|
||||
|
||||
const aa = arena.allocator();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
buf.ensureTotalCapacity(arena.allocator(), message_len) catch |err| {
|
||||
log.err(.cdp, "inspector buffer", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
// reserve 10 bytes for websocket header
|
||||
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||
try buf.appendSlice(aa, &.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||
|
||||
// -1 because we dont' want the closing brace '}'
|
||||
buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]);
|
||||
buf.appendSliceAssumeCapacity(field);
|
||||
buf.appendSliceAssumeCapacity(session_id);
|
||||
buf.appendSliceAssumeCapacity("\"}");
|
||||
std.debug.assert(buf.items.len == message_len);
|
||||
try cbor.cborToJson(str.bytes(), buf.writer(aa));
|
||||
|
||||
std.debug.assert(buf.getLast() == '}');
|
||||
|
||||
// We need to inject the session_id
|
||||
// First, we strip out the closing '}'
|
||||
buf.items.len -= 1;
|
||||
|
||||
// Next we inject the session id field + value
|
||||
try buf.appendSlice(aa, ",\"sessionId\":\"");
|
||||
try buf.appendSlice(aa, session_id);
|
||||
|
||||
// Finally, we re-close the object. Smooth.
|
||||
try buf.appendSlice(aa, "\"}");
|
||||
|
||||
try cdp.client.sendJSONRaw(arena, buf);
|
||||
}
|
||||
@@ -555,8 +538,8 @@ const IsolatedWorld = struct {
|
||||
self.executor.deinit();
|
||||
}
|
||||
pub fn removeContext(self: *IsolatedWorld) !void {
|
||||
if (self.executor.js_context == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.removeJsContext();
|
||||
if (self.executor.scope == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.endScope();
|
||||
}
|
||||
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
@@ -565,8 +548,8 @@ const IsolatedWorld = struct {
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
|
||||
_ = try self.executor.createJsContext(&page.window, page, {}, false);
|
||||
if (self.executor.scope != null) return error.Only1IsolatedContextSupported;
|
||||
_ = try self.executor.startScope(&page.window, page, {}, false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -259,13 +259,13 @@ fn resolveNode(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
var js_context = page.main_context;
|
||||
var scope = page.scope;
|
||||
if (params.executionContextId) |context_id| {
|
||||
if (js_context.v8_context.debugContextId() != context_id) {
|
||||
if (scope.context.debugContextId() != context_id) {
|
||||
var isolated_world = bc.isolated_world orelse return error.ContextNotFound;
|
||||
js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound);
|
||||
scope = &(isolated_world.executor.scope orelse return error.ContextNotFound);
|
||||
|
||||
if (js_context.v8_context.debugContextId() != context_id) return error.ContextNotFound;
|
||||
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ fn resolveNode(cmd: anytype) !void {
|
||||
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||
// So we use the Node.Union when retrieve the value from the environment
|
||||
const remote_object = try bc.inspector.getRemoteObject(
|
||||
js_context,
|
||||
scope,
|
||||
params.objectGroup orelse "",
|
||||
try dom_node.Node.toInterface(node._node),
|
||||
);
|
||||
@@ -368,7 +368,7 @@ fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backen
|
||||
if (object_id) |object_id_| {
|
||||
// Retrieve the object from which ever context it is in.
|
||||
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_);
|
||||
return try browser_context.node_registry.register(@alignCast(@ptrCast(parser_node)));
|
||||
return try browser_context.node_registry.register(@ptrCast(parser_node));
|
||||
}
|
||||
return error.MissingParams;
|
||||
}
|
||||
|
||||
@@ -21,60 +21,14 @@ const Page = @import("../../browser/page.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
dispatchKeyEvent,
|
||||
dispatchMouseEvent,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
|
||||
fn dispatchKeyEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
type: Type,
|
||||
key: []const u8 = "",
|
||||
code: []const u8 = "",
|
||||
modifiers: u4 = 0,
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
const Type = enum {
|
||||
keyDown,
|
||||
keyUp,
|
||||
rawKeyDown,
|
||||
char,
|
||||
};
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
|
||||
// quickly ignore types we know we don't handle
|
||||
switch (params.type) {
|
||||
.keyUp, .rawKeyDown, .char => return,
|
||||
.keyDown => {},
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
const keyboard_event = Page.KeyboardEvent{
|
||||
.key = params.key,
|
||||
.code = params.code,
|
||||
.type = switch (params.type) {
|
||||
.keyDown => .keydown,
|
||||
else => unreachable,
|
||||
},
|
||||
.alt = params.modifiers & 1 == 1,
|
||||
.ctrl = params.modifiers & 2 == 2,
|
||||
.meta = params.modifiers & 4 == 4,
|
||||
.shift = params.modifiers & 8 == 8,
|
||||
};
|
||||
try page.keyboardEvent(keyboard_event);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||
fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
|
||||
@@ -84,26 +84,6 @@ fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: s
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn httpRequestFail(arena: Allocator, bc: anytype, request: *const Notification.RequestFail) !void {
|
||||
// It's possible that the request failed because we aborted when the client
|
||||
// sent Target.closeTarget. In that case, bc.session_id will be cleared
|
||||
// already, and we can skip sending these messages to the client.
|
||||
const session_id = bc.session_id orelse return;
|
||||
|
||||
// Isn't possible to do a network request within a Browser (which our
|
||||
// notification is tied to), without a page.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||
try bc.cdp.sendEvent("Network.loadingFailed", .{
|
||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
|
||||
// Seems to be what chrome answers with. I assume it depends on the type of error?
|
||||
.type = "Ping",
|
||||
.errorText = request.err,
|
||||
.canceled = false,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notification.RequestStart) !void {
|
||||
// Isn't possible to do a network request within a Browser (which our
|
||||
// notification is tied to), without a page.
|
||||
|
||||
@@ -117,14 +117,14 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try pageCreated(bc, page);
|
||||
const js_context = &world.executor.js_context.?;
|
||||
const scope = &world.executor.scope.?;
|
||||
|
||||
// Create the auxdata json for the contextCreated event
|
||||
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
||||
bc.inspector.contextCreated(js_context, world.name, "", aux_data, false);
|
||||
bc.inspector.contextCreated(scope, world.name, "", aux_data, false);
|
||||
|
||||
return cmd.sendResult(.{ .executionContextId = js_context.v8_context.debugContextId() }, .{});
|
||||
return cmd.sendResult(.{ .executionContextId = scope.context.debugContextId() }, .{});
|
||||
}
|
||||
|
||||
fn navigate(cmd: anytype) !void {
|
||||
@@ -163,11 +163,6 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
var cdp = bc.cdp;
|
||||
|
||||
if (event.opts.reason != .address_bar) {
|
||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
||||
}
|
||||
|
||||
const loader_id = bc.loader_id;
|
||||
const target_id = bc.target_id orelse unreachable;
|
||||
const session_id = bc.session_id orelse unreachable;
|
||||
@@ -253,7 +248,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.main_context,
|
||||
page.scope,
|
||||
"",
|
||||
try page.origin(arena),
|
||||
aux_data,
|
||||
@@ -264,7 +259,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
||||
bc.inspector.contextCreated(
|
||||
&isolated_world.executor.js_context.?,
|
||||
&isolated_world.executor.scope.?,
|
||||
isolated_world.name,
|
||||
"://",
|
||||
aux_json,
|
||||
@@ -286,7 +281,7 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||
try isolated_world.createContext(page);
|
||||
|
||||
const polyfill = @import("../../browser/polyfill/polyfill.zig");
|
||||
try polyfill.load(bc.arena, &isolated_world.executor.js_context.?);
|
||||
try polyfill.load(bc.arena, &isolated_world.executor.scope.?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// the result to return is handled directly by the inspector.
|
||||
bc.callInspector(cmd.input.json);
|
||||
return bc.callInspector(cmd.arena, cmd.input.json);
|
||||
}
|
||||
|
||||
fn logInspector(cmd: anytype, action: anytype) !void {
|
||||
|
||||
@@ -127,7 +127,7 @@ fn createTarget(cmd: anytype) !void {
|
||||
{
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.main_context,
|
||||
page.scope,
|
||||
"",
|
||||
try page.origin(cmd.arena),
|
||||
aux_data,
|
||||
@@ -220,7 +220,7 @@ fn closeTarget(cmd: anytype) !void {
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.removePage();
|
||||
try bc.session.removePage();
|
||||
if (bc.isolated_world) |*world| {
|
||||
world.deinit();
|
||||
bc.isolated_world = null;
|
||||
|
||||
@@ -113,18 +113,10 @@ pub const Client = struct {
|
||||
loop: *Loop,
|
||||
opts: RequestOpts,
|
||||
) !void {
|
||||
|
||||
// See the page's DelayedNavitation for why we're doing this. TL;DR -
|
||||
// we need to keep 1 slot available for the blocking page navigation flow
|
||||
// (Almost worth keeping a dedicate State just for that flow, but keep
|
||||
// thinking we need a more permanent solution (i.e. making everything
|
||||
// non-blocking).
|
||||
if (self.freeSlotCount() > 1) {
|
||||
if (self.state_pool.acquireOrNull()) |state| {
|
||||
// if we have state ready, we can skip the loop and immediately
|
||||
// kick this request off.
|
||||
return self.asyncRequestReady(method, uri, ctx, callback, state, opts);
|
||||
}
|
||||
if (self.state_pool.acquireOrNull()) |state| {
|
||||
// if we have state ready, we can skip the loop and immediately
|
||||
// kick this request off.
|
||||
return self.asyncRequestReady(method, uri, ctx, callback, state, opts);
|
||||
}
|
||||
|
||||
// This cannot be a client-owned MemoryPool. The page can end before
|
||||
@@ -182,10 +174,6 @@ pub const Client = struct {
|
||||
.client = self,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn freeSlotCount(self: *Client) usize {
|
||||
return self.state_pool.freeSlotCount();
|
||||
}
|
||||
};
|
||||
|
||||
const RequestOpts = struct {
|
||||
@@ -366,7 +354,6 @@ pub const Request = struct {
|
||||
// Because of things like redirects and error handling, it is possible for
|
||||
// the notification functions to be called multiple times, so we guard them
|
||||
// with these booleans
|
||||
_notified_fail: bool,
|
||||
_notified_start: bool,
|
||||
_notified_complete: bool,
|
||||
|
||||
@@ -427,7 +414,6 @@ pub const Request = struct {
|
||||
._keepalive = false,
|
||||
._redirect_count = 0,
|
||||
._has_host_header = false,
|
||||
._notified_fail = false,
|
||||
._notified_start = false,
|
||||
._notified_complete = false,
|
||||
._connection_from_keepalive = false,
|
||||
@@ -442,7 +428,6 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
pub fn abort(self: *Request) void {
|
||||
self.requestFailed("aborted");
|
||||
const aborter = self._aborter orelse {
|
||||
self.deinit();
|
||||
return;
|
||||
@@ -570,10 +555,6 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
fn doSendSync(self: *Request, use_pool: bool) anyerror!Response {
|
||||
// https://github.com/ziglang/zig/issues/20369
|
||||
// errdefer |err| self.requestFailed(@errorName(err));
|
||||
errdefer self.requestFailed("network error");
|
||||
|
||||
if (use_pool) {
|
||||
if (self.findExistingConnection(true)) |connection| {
|
||||
self._connection = connection;
|
||||
@@ -866,19 +847,6 @@ pub const Request = struct {
|
||||
});
|
||||
}
|
||||
|
||||
fn requestFailed(self: *Request, err: []const u8) void {
|
||||
const notification = self.notification orelse return;
|
||||
if (self._notified_fail) {
|
||||
return;
|
||||
}
|
||||
self._notified_fail = true;
|
||||
notification.dispatch(.http_request_fail, &.{
|
||||
.id = self.id,
|
||||
.err = err,
|
||||
.url = self.request_uri,
|
||||
});
|
||||
}
|
||||
|
||||
fn requestCompleted(self: *Request, response: ResponseHeader) void {
|
||||
const notification = self.notification orelse return;
|
||||
if (self._notified_complete) {
|
||||
@@ -1322,8 +1290,6 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
self.handler.onHttpResponse(err) catch {};
|
||||
// just to be safe
|
||||
self.request._keepalive = false;
|
||||
|
||||
self.request.requestFailed(@errorName(err));
|
||||
self.request.deinit();
|
||||
}
|
||||
|
||||
@@ -2543,12 +2509,6 @@ const StatePool = struct {
|
||||
allocator.free(self.states);
|
||||
}
|
||||
|
||||
pub fn freeSlotCount(self: *StatePool) usize {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
return self.available;
|
||||
}
|
||||
|
||||
pub fn acquireWait(self: *StatePool) *State {
|
||||
const states = self.states;
|
||||
|
||||
@@ -3040,14 +3000,8 @@ test "HttpClient: async connect error" {
|
||||
.{},
|
||||
);
|
||||
|
||||
for (0..10) |_| {
|
||||
try loop.io.run_for_ns(std.time.ns_per_ms * 10);
|
||||
if (reset.isSet()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return error.Timeout;
|
||||
}
|
||||
try loop.io.run_for_ns(std.time.ns_per_ms);
|
||||
try reset.timedWait(std.time.ns_per_s);
|
||||
}
|
||||
|
||||
test "HttpClient: async no body" {
|
||||
|
||||
@@ -522,7 +522,7 @@ test {
|
||||
|
||||
var test_wg: std.Thread.WaitGroup = .{};
|
||||
test "tests:beforeAll" {
|
||||
try parser.init(std.testing.allocator);
|
||||
try parser.init();
|
||||
log.opts.level = .err;
|
||||
log.opts.format = .logfmt;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try polyfill.load(arena, runner.page.main_context);
|
||||
try polyfill.load(arena, runner.page.scope);
|
||||
|
||||
// loop over the scripts.
|
||||
const doc = parser.documentHTMLToDocument(runner.page.window.document);
|
||||
@@ -155,7 +155,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
{
|
||||
// wait for all async executions
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(runner.page.main_context);
|
||||
try_catch.init(runner.page.scope);
|
||||
defer try_catch.deinit();
|
||||
try runner.page.loop.run();
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ pub const Notification = struct {
|
||||
page_created: List = .{},
|
||||
page_navigate: List = .{},
|
||||
page_navigated: List = .{},
|
||||
http_request_fail: List = .{},
|
||||
http_request_start: List = .{},
|
||||
http_request_complete: List = .{},
|
||||
notification_created: List = .{},
|
||||
@@ -70,7 +69,6 @@ pub const Notification = struct {
|
||||
page_created: *page.Page,
|
||||
page_navigate: *const PageNavigate,
|
||||
page_navigated: *const PageNavigated,
|
||||
http_request_fail: *const RequestFail,
|
||||
http_request_start: *const RequestStart,
|
||||
http_request_complete: *const RequestComplete,
|
||||
notification_created: *Notification,
|
||||
@@ -99,12 +97,6 @@ pub const Notification = struct {
|
||||
has_body: bool,
|
||||
};
|
||||
|
||||
pub const RequestFail = struct {
|
||||
id: usize,
|
||||
url: *const std.Uri,
|
||||
err: []const u8,
|
||||
};
|
||||
|
||||
pub const RequestComplete = struct {
|
||||
id: usize,
|
||||
url: *const std.Uri,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,13 +81,12 @@ pub const Loop = struct {
|
||||
|
||||
// run tail events. We do run the tail events to ensure all the
|
||||
// contexts are correcly free.
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
while (self.hasPendinEvents()) {
|
||||
self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
if (comptime CANCEL_SUPPORTED) {
|
||||
self.io.cancel_all();
|
||||
}
|
||||
@@ -97,6 +96,21 @@ pub const Loop = struct {
|
||||
self.cancelled.deinit(self.alloc);
|
||||
}
|
||||
|
||||
// We can shutdown once all the pending network IO is complete.
|
||||
// In debug mode we also wait until al the pending timeouts are complete
|
||||
// but we only do this so that the `timeoutCallback` can free all allocated
|
||||
// memory and we won't report a leak.
|
||||
fn hasPendinEvents(self: *const Self) bool {
|
||||
if (self.pending_network_count > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (builtin.mode != .Debug) {
|
||||
return false;
|
||||
}
|
||||
return self.pending_timeout_count > 0;
|
||||
}
|
||||
|
||||
// Retrieve all registred I/O events completed by OS kernel,
|
||||
// and execute sequentially their callbacks.
|
||||
// Stops when there is no more I/O events registered on the loop.
|
||||
@@ -107,11 +121,9 @@ pub const Loop = struct {
|
||||
self.stopping = true;
|
||||
defer self.stopping = false;
|
||||
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
};
|
||||
while (self.pending_network_count > 0) {
|
||||
try self.io.run_for_ns(10 * std.time.ns_per_ms);
|
||||
// at each iteration we might have new events registred by previous callbacks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
|
||||
return struct {
|
||||
env: *Env,
|
||||
js_context: *Env.JsContext,
|
||||
scope: *Env.Scope,
|
||||
executor: Env.ExecutionWorld,
|
||||
|
||||
pub const Env = js.Env(State, struct {
|
||||
@@ -48,7 +48,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
self.executor = try self.env.newExecutionWorld();
|
||||
errdefer self.executor.deinit();
|
||||
|
||||
self.js_context = try self.executor.createJsContext(
|
||||
self.scope = try self.executor.startScope(
|
||||
if (Global == void) &default_global else global,
|
||||
state,
|
||||
{},
|
||||
@@ -68,10 +68,10 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
|
||||
for (cases, 0..) |case, i| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.js_context);
|
||||
try_catch.init(self.scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = self.js_context.exec(case.@"0", null) catch |err| {
|
||||
const value = self.scope.exec(case.@"0", null) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
if (isExpectedTypeError(case.@"1", msg)) {
|
||||
|
||||
@@ -211,16 +211,14 @@ pub const Document = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
pub fn init(html: []const u8) !Document {
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
parser.deinit();
|
||||
try parser.init(arena.allocator());
|
||||
try parser.init();
|
||||
|
||||
var fbs = std.io.fixedBufferStream(html);
|
||||
const Elements = @import("browser/html/elements.zig");
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement);
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
||||
|
||||
return .{
|
||||
.arena = arena,
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.doc = html_doc,
|
||||
};
|
||||
}
|
||||
@@ -421,17 +419,17 @@ pub const JsRunner = struct {
|
||||
const RunOpts = struct {};
|
||||
pub const Case = std.meta.Tuple(&.{ []const u8, ?[]const u8 });
|
||||
pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void {
|
||||
const js_context = self.page.main_context;
|
||||
const scope = self.page.scope;
|
||||
const arena = self.page.arena;
|
||||
|
||||
const start = try std.time.Instant.now();
|
||||
|
||||
for (cases, 0..) |case, i| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
try_catch.init(scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = js_context.exec(case.@"0", null) catch |err| {
|
||||
const value = scope.exec(case.@"0", null) catch |err| {
|
||||
if (try try_catch.err(arena)) |msg| {
|
||||
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
|
||||
}
|
||||
@@ -455,14 +453,14 @@ pub const JsRunner = struct {
|
||||
}
|
||||
|
||||
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
|
||||
const js_context = self.page.main_context;
|
||||
const scope = self.page.scope;
|
||||
const arena = self.page.arena;
|
||||
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
try_catch.init(scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
return js_context.exec(src, name) catch |err| {
|
||||
return scope.exec(src, name) catch |err| {
|
||||
if (try try_catch.err(arena)) |msg| {
|
||||
err_msg.* = msg;
|
||||
std.debug.print("Error running script: {s}\n", .{msg});
|
||||
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: f22449c52e...614187b0aa
Reference in New Issue
Block a user