1 Commits

Author SHA1 Message Date
Karl Seguin
d9ac1fa3bc Reduce copying of incoming and outgoing inspector messages.
When inspector emits a message, to be sent to the client, we copy those bytes a
number of times. First, V8 serializes the message to CBOR. Next, it converts it
to JSON. We then copy this into a C++ string, then into a Zig slice. We create
one final copy (with websocket framing) to add to the write queue.

Something similar, but a little less extreme, happens with incoming messages.

By supporting CBOR messages directly, we not only reduce the amount of copying,
but also leverage our [more tightly scoped and re-used] arenas.

CBOR is essentially a standardized MessagePack. Two functions, jsonToCbor and
cborToJson have been introduced to take our incoming JSON message and convert it
to CBOR and, vice-versa. V8 automatically detects that the message is CBOR and,
if the incoming message is CBOR, the outgoing message is CBOR also.

While v8 is spec-compliant, it has specific expectations and behavior. For
example, it never emits a fixed-length array / map - it's always an infinite
array / map (with a special "break" code at the end). For this reason, our
implementation is not complete, but rather designed to work with what v8 does
and expects.

Another example of this is, and I don't understand why, some of the
incoming messages have a "params" field. V8 requires this to be a CBOR embedded
data field (that is, CBOR embedded into CBOR). If we pass an array directly,
while semantically the same, it'll fail. I guess this is how Chrome serializes
the data, and rather than just reading the data as-is, v8 asserts that it's
encoded in a particularly flavor. Weird. But we have to accommodate that.
2025-06-08 21:08:13 +08:00
40 changed files with 1029 additions and 1057 deletions

View File

@@ -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" },

View File

@@ -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| {

View File

@@ -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;
}

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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)),
};
}

View File

@@ -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);

View File

@@ -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;
}
};

View File

@@ -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, &copy.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
}, .{});

View File

@@ -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);

View File

@@ -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");

View File

@@ -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(&params, &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);
}

View File

@@ -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);
}

View File

@@ -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(&.{
.{

View File

@@ -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 });

View File

@@ -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);
}
};

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
View 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);
}

View 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);
}

View 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);
}

View File

@@ -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);
}
};

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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.?);
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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" {

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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)) {

View File

@@ -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});