From 7d9951aa3c211dde31a5ddc245de6c49e852bd6e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 27 May 2025 18:35:35 +0800 Subject: [PATCH 1/2] Replace SessionState directly with the Page. --- src/browser/console/console.zig | 42 +-- src/browser/cssom/css_style_declaration.zig | 34 +-- src/browser/dom/comment.zig | 6 +- src/browser/dom/document.zig | 31 ++- src/browser/dom/document_fragment.zig | 6 +- src/browser/dom/element.zig | 56 ++-- src/browser/dom/event_target.zig | 10 +- src/browser/dom/intersection_observer.zig | 32 ++- src/browser/dom/mutation_observer.zig | 6 +- src/browser/dom/node.zig | 6 +- src/browser/dom/processing_instruction.zig | 6 +- src/browser/dom/text.zig | 6 +- src/browser/dom/tree_walker.zig | 2 +- src/browser/dump.zig | 3 + src/browser/env.zig | 55 +--- src/browser/html/document.zig | 70 ++--- src/browser/html/elements.zig | 128 ++++----- src/browser/html/location.zig | 34 +-- src/browser/html/select.zig | 10 +- src/browser/html/window.zig | 36 +-- src/browser/page.zig | 274 +++++++++++--------- src/browser/polyfill/fetch.zig | 26 +- src/browser/storage/cookie.zig | 2 +- src/browser/url/url.zig | 42 +-- src/browser/xhr/event_target.zig | 26 +- src/browser/xhr/form_data.zig | 32 +-- src/browser/xhr/xhr.zig | 20 +- src/browser/xmlserializer/xmlserializer.zig | 6 +- src/cdp/cdp.zig | 2 +- src/cdp/domains/dom.zig | 6 +- src/main_wpt.zig | 10 +- src/runtime/testing.zig | 2 +- src/telemetry/telemetry.zig | 2 +- src/testing.zig | 136 ++++------ 34 files changed, 562 insertions(+), 603 deletions(-) diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig index 3e3fd02b..1edbc612 100644 --- a/src/browser/console/console.zig +++ b/src/browser/console/console.zig @@ -20,7 +20,7 @@ const std = @import("std"); const builtin = @import("builtin"); const JsObject = @import("../env.zig").Env.JsObject; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const log = if (builtin.is_test) &test_capture else @import("../../log.zig"); @@ -29,49 +29,49 @@ pub const Console = struct { timers: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{}, - pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void { + pub fn _log(_: *const Console, values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } - log.info(.console, "info", .{ .args = try serializeValues(values, state) }); + log.info(.console, "info", .{ .args = try serializeValues(values, page) }); } - pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void { - return console._log(values, state); + pub fn _info(console: *const Console, values: []JsObject, page: *Page) !void { + return console._log(values, page); } - pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void { + pub fn _debug(_: *const Console, values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } - log.debug(.console, "debug", .{ .args = try serializeValues(values, state) }); + log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); } - pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void { + pub fn _warn(_: *const Console, values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } - log.warn(.console, "warn", .{ .args = try serializeValues(values, state) }); + log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); } - pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void { + pub fn _error(_: *const Console, values: []JsObject, page: *Page) !void { if (values.len == 0) { return; } - log.info(.console, "error", .{ .args = try serializeValues(values, state) }); + log.info(.console, "error", .{ .args = try serializeValues(values, page) }); } pub fn _clear(_: *const Console) void {} - pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void { + pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { const label = label_ orelse "default"; - const gop = try self.counts.getOrPut(state.arena, label); + const gop = try self.counts.getOrPut(page.arena, label); var current: u32 = 0; if (gop.found_existing) { current = gop.value_ptr.*; } else { - gop.key_ptr.* = try state.arena.dupe(u8, label); + gop.key_ptr.* = try page.arena.dupe(u8, label); } const count = current + 1; @@ -89,15 +89,15 @@ pub const Console = struct { log.info(.console, "count reset", .{ .label = label, .count = kv.value }); } - pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void { + pub fn _time(self: *Console, label_: ?[]const u8, page: *Page) !void { const label = label_ orelse "default"; - const gop = try self.timers.getOrPut(state.arena, label); + const gop = try self.timers.getOrPut(page.arena, label); if (gop.found_existing) { log.info(.console, "duplicate timer", .{ .label = label }); return; } - gop.key_ptr.* = try state.arena.dupe(u8, label); + gop.key_ptr.* = try page.arena.dupe(u8, label); gop.value_ptr.* = timestamp(); } @@ -122,19 +122,19 @@ pub const Console = struct { log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); } - pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void { + pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, page: *Page) !void { if (assertion.isTruthy()) { return; } var serialized_values: []const u8 = ""; if (values.len > 0) { - serialized_values = try serializeValues(values, state); + serialized_values = try serializeValues(values, page); } log.info(.console, "assertion failed", .{ .values = serialized_values }); } - fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 { - const arena = state.call_arena; + fn serializeValues(values: []JsObject, page: *Page) ![]const u8 { + const arena = page.call_arena; var arr: std.ArrayListUnmanaged(u8) = .{}; try arr.appendSlice(arena, try values[0].toString()); for (values[1..]) |value| { diff --git a/src/browser/cssom/css_style_declaration.zig b/src/browser/cssom/css_style_declaration.zig index 62779f5f..1e980fb2 100644 --- a/src/browser/cssom/css_style_declaration.zig +++ b/src/browser/cssom/css_style_declaration.zig @@ -20,7 +20,7 @@ const std = @import("std"); const CSSParser = @import("./css_parser.zig").CSSParser; const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; pub const Interfaces = .{ CSSStyleDeclaration, @@ -47,17 +47,17 @@ pub const CSSStyleDeclaration = struct { return self._getPropertyValue("float"); } - pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, state: *SessionState) !void { + pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void { const final_value = value orelse ""; - return self._setProperty("float", final_value, null, state); + return self._setProperty("float", final_value, null, page); } - pub fn get_cssText(self: *const CSSStyleDeclaration, state: *SessionState) ![]const u8 { + pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 { var buffer: std.ArrayListUnmanaged(u8) = .empty; - const writer = buffer.writer(state.call_arena); + const writer = buffer.writer(page.call_arena); for (self.order.items) |name| { const prop = self.store.get(name).?; - const escaped = try CSSValueAnalyzer.escapeCSSValue(state.call_arena, prop.value); + const escaped = try CSSValueAnalyzer.escapeCSSValue(page.call_arena, prop.value); try writer.print("{s}: {s}", .{ name, escaped }); if (prop.priority) try writer.writeAll(" !important"); try writer.writeAll("; "); @@ -66,18 +66,18 @@ pub const CSSStyleDeclaration = struct { } // TODO Propagate also upward to parent node - pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, state: *SessionState) !void { + pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void { self.store.clearRetainingCapacity(); self.order.clearRetainingCapacity(); // call_arena is safe here, because _setProperty will dupe the name - // using the state's longer-living arena. - const declarations = try CSSParser.parseDeclarations(state.call_arena, text); + // using the page's longer-living arena. + const declarations = try CSSParser.parseDeclarations(page.call_arena, text); for (declarations) |decl| { if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue; const priority: ?[]const u8 = if (decl.is_important) "important" else null; - try self._setProperty(decl.name, decl.value, priority, state); + try self._setProperty(decl.name, decl.value, priority, page); } } @@ -119,19 +119,19 @@ pub const CSSStyleDeclaration = struct { break; } } - // safe to return, since it's in our state.arena + // safe to return, since it's in our page.arena return prop.value.value; } - pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, state: *SessionState) !void { - const owned_value = try state.arena.dupe(u8, value); + pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void { + const owned_value = try page.arena.dupe(u8, value); const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important"); - const gop = try self.store.getOrPut(state.arena, name); + const gop = try self.store.getOrPut(page.arena, name); if (!gop.found_existing) { - const owned_name = try state.arena.dupe(u8, name); + const owned_name = try page.arena.dupe(u8, name); gop.key_ptr.* = owned_name; - try self.order.append(state.arena, owned_name); + try self.order.append(page.arena, owned_name); } gop.value_ptr.* = .{ .value = owned_value, .priority = is_important }; @@ -177,7 +177,7 @@ test "CSSOM.CSSStyleDeclaration" { .{ "style.setProperty('color', 'green')", "undefined" }, .{ "style.getPropertyValue('color')", "green" }, .{ "style.length", "4" }, - .{ "style.color", "green"}, + .{ "style.color", "green" }, .{ "style.setProperty('padding', '10px', 'important')", "undefined" }, .{ "style.getPropertyValue('padding')", "10px" }, diff --git a/src/browser/dom/comment.zig b/src/browser/dom/comment.zig index 19aa9781..62c6bd4a 100644 --- a/src/browser/dom/comment.zig +++ b/src/browser/dom/comment.zig @@ -20,7 +20,7 @@ const parser = @import("../netsurf.zig"); const CharacterData = @import("character_data.zig").CharacterData; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; // https://dom.spec.whatwg.org/#interface-comment pub const Comment = struct { @@ -28,9 +28,9 @@ pub const Comment = struct { pub const prototype = *CharacterData; pub const subtype = .node; - pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment { + pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Comment { return parser.documentCreateComment( - parser.documentHTMLToDocument(state.window.document), + parser.documentHTMLToDocument(page.window.document), data orelse "", ); } diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 03d15616..a987717b 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Node = @import("node.zig").Node; const NodeList = @import("nodelist.zig").NodeList; @@ -42,14 +42,14 @@ pub const Document = struct { pub const prototype = *Node; pub const subtype = .node; - pub fn constructor(state: *const SessionState) !*parser.DocumentHTML { + pub fn constructor(page: *const Page) !*parser.DocumentHTML { const doc = try parser.documentCreateDocument( - try parser.documentHTMLGetTitle(state.window.document), + try parser.documentHTMLGetTitle(page.window.document), ); // we have to work w/ document instead of html document. const ddoc = parser.documentHTMLToDocument(doc); - const ccur = parser.documentHTMLToDocument(state.window.document); + const ccur = parser.documentHTMLToDocument(page.window.document); try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur)); try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur)); @@ -141,18 +141,17 @@ pub const Document = struct { pub fn _getElementsByTagName( self: *parser.Document, tag_name: []const u8, - state: *SessionState, + page: *Page, ) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true); + return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true); } pub fn _getElementsByClassName( self: *parser.Document, classNames: []const u8, - state: *SessionState, + page: *Page, ) !collection.HTMLCollection { - const allocator = state.arena; - return try collection.HTMLCollectionByClassName(allocator, parser.documentToNode(self), classNames, true); + return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, true); } pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment { @@ -214,20 +213,18 @@ pub const Document = struct { return 1; } - pub fn _querySelector(self: *parser.Document, selector: []const u8, state: *SessionState) !?ElementUnion { + pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion { if (selector.len == 0) return null; - const allocator = state.arena; - const n = try css.querySelector(allocator, parser.documentToNode(self), selector); + const n = try css.querySelector(page.arena, parser.documentToNode(self), selector); if (n == null) return null; return try Element.toInterface(parser.nodeToElement(n.?)); } - pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, state: *SessionState) !NodeList { - const allocator = state.arena; - return css.querySelectorAll(allocator, parser.documentToNode(self), selector); + pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, page: *Page) !NodeList { + return css.querySelectorAll(page.arena, parser.documentToNode(self), selector); } pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void { @@ -249,7 +246,9 @@ pub const Document = struct { const testing = @import("../../testing.zig"); test "Browser.DOM.Document" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + var runner = try testing.jsRunner(testing.tracking_allocator, .{ + .url = "about:blank", + }); defer runner.deinit(); try runner.testCases(&.{ diff --git a/src/browser/dom/document_fragment.zig b/src/browser/dom/document_fragment.zig index 7bb0e5c1..0085ec33 100644 --- a/src/browser/dom/document_fragment.zig +++ b/src/browser/dom/document_fragment.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Node = @import("node.zig").Node; @@ -27,9 +27,9 @@ pub const DocumentFragment = struct { pub const prototype = *Node; pub const subtype = .node; - pub fn constructor(state: *const SessionState) !*parser.DocumentFragment { + pub fn constructor(page: *const Page) !*parser.DocumentFragment { return parser.documentCreateDocumentFragment( - parser.documentHTMLToDocument(state.window.document), + parser.documentHTMLToDocument(page.window.document), ); } diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 976025d6..12e5f39d 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const css = @import("css.zig"); const log = @import("../../log.zig"); @@ -102,14 +102,14 @@ pub const Element = struct { return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable; } - pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 { - var buf = std.ArrayList(u8).init(state.arena); + pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 { + var buf = std.ArrayList(u8).init(page.arena); try dump.writeChildren(parser.elementToNode(self), buf.writer()); return buf.items; } - pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 { - var buf = std.ArrayList(u8).init(state.arena); + pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 { + var buf = std.ArrayList(u8).init(page.arena); try dump.writeNode(parser.elementToNode(self), buf.writer()); return buf.items; } @@ -138,10 +138,10 @@ pub const Element = struct { // The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector. // Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null. - pub fn _closest(self: *parser.Element, selector: []const u8, state: *SessionState) !?*parser.Element { + pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element { const cssParse = @import("../css/css.zig").parse; const CssNodeWrap = @import("../css/libdom.zig").Node; - const select = try cssParse(state.call_arena, selector, .{}); + const select = try cssParse(page.call_arena, selector, .{}); var current: CssNodeWrap = .{ .node = parser.elementToNode(self) }; while (true) { @@ -249,10 +249,10 @@ pub const Element = struct { pub fn _getElementsByTagName( self: *parser.Element, tag_name: []const u8, - state: *SessionState, + page: *Page, ) !collection.HTMLCollection { return try collection.HTMLCollectionByTagName( - state.arena, + page.arena, parser.elementToNode(self), tag_name, false, @@ -262,10 +262,10 @@ pub const Element = struct { pub fn _getElementsByClassName( self: *parser.Element, classNames: []const u8, - state: *SessionState, + page: *Page, ) !collection.HTMLCollection { return try collection.HTMLCollectionByClassName( - state.arena, + page.arena, parser.elementToNode(self), classNames, false, @@ -328,18 +328,18 @@ pub const Element = struct { } } - pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union { + pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union { if (selector.len == 0) return null; - const n = try css.querySelector(state.arena, parser.elementToNode(self), selector); + const n = try css.querySelector(page.arena, parser.elementToNode(self), selector); if (n == null) return null; return try toInterface(parser.nodeToElement(n.?)); } - pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList { - return css.querySelectorAll(state.arena, parser.elementToNode(self), selector); + pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, page: *Page) !NodeList { + return css.querySelectorAll(page.arena, parser.elementToNode(self), selector); } pub fn _prepend(self: *parser.Element, nodes: []const Node.NodeOrText) !void { @@ -366,40 +366,40 @@ pub const Element = struct { // A DOMRect object providing information about the size of an element and its position relative to the viewport. // Returns a 0 DOMRect object if the element is eventually detached from the main window - pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect { + pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect { // Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes. const root = try parser.nodeGetRootNode(parser.elementToNode(self)); - if (root != parser.documentToNode(parser.documentHTMLToDocument(state.window.document))) { + if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) { return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 }; } - return state.renderer.getRect(self); + return page.renderer.getRect(self); } // Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client. // We do not render so it only always return the element's bounding rect. // Returns an empty array if the element is eventually detached from the main window - pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![]DOMRect { + pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect { const root = try parser.nodeGetRootNode(parser.elementToNode(self)); - if (root != parser.documentToNode(parser.documentHTMLToDocument(state.window.document))) { + if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) { return &.{}; } - const heap_ptr = try state.call_arena.create(DOMRect); - heap_ptr.* = try state.renderer.getRect(self); + const heap_ptr = try page.call_arena.create(DOMRect); + heap_ptr.* = try page.renderer.getRect(self); return heap_ptr[0..1]; } - pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 { - return state.renderer.width(); + pub fn get_clientWidth(_: *parser.Element, page: *Page) u32 { + return page.renderer.width(); } - pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 { - return state.renderer.height(); + pub fn get_clientHeight(_: *parser.Element, page: *Page) u32 { + return page.renderer.height(); } - pub fn _matches(self: *parser.Element, selectors: []const u8, state: *SessionState) !bool { + pub fn _matches(self: *parser.Element, selectors: []const u8, page: *Page) !bool { const cssParse = @import("../css/css.zig").parse; const CssNodeWrap = @import("../css/libdom.zig").Node; - const s = try cssParse(state.call_arena, selectors, .{}); + const s = try cssParse(page.call_arena, selectors, .{}); return s.match(CssNodeWrap{ .node = parser.elementToNode(self) }); } diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index f25c7e76..bd320385 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -18,7 +18,7 @@ const Env = @import("../env.zig").Env; const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const EventHandler = @import("../events/event.zig").EventHandler; @@ -59,7 +59,7 @@ pub const EventTarget = struct { typ: []const u8, cbk: Env.Function, opts_: ?AddEventListenerOpts, - state: *SessionState, + page: *Page, ) !void { var capture = false; if (opts_) |opts| { @@ -91,7 +91,7 @@ pub const EventTarget = struct { return; } - const eh = try EventHandler.init(state.arena, try cbk.withThis(self)); + const eh = try EventHandler.init(page.arena, try cbk.withThis(self)); try parser.eventTargetAddEventListener( self, @@ -132,10 +132,6 @@ pub const EventTarget = struct { pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { return try parser.eventTargetDispatchEvent(self, event); } - - pub fn deinit(self: *parser.EventTarget, state: *SessionState) void { - parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable; - } }; const testing = @import("../../testing.zig"); diff --git a/src/browser/dom/intersection_observer.zig b/src/browser/dom/intersection_observer.zig index 18b02508..17e29787 100644 --- a/src/browser/dom/intersection_observer.zig +++ b/src/browser/dom/intersection_observer.zig @@ -20,7 +20,7 @@ const std = @import("std"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Env = @import("../env.zig").Env; const Element = @import("element.zig").Element; @@ -39,17 +39,17 @@ pub const Interfaces = .{ // The returned Entries are phony, they always indicate full intersection. // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver pub const IntersectionObserver = struct { + page: *Page, callback: Env.Function, options: IntersectionObserverOptions, - state: *SessionState, observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry), // new IntersectionObserver(callback) // new IntersectionObserver(callback, options) [not supported yet] - pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver { + pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver { var options = IntersectionObserverOptions{ - .root = parser.documentToNode(parser.documentHTMLToDocument(state.window.document)), + .root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)), .rootMargin = "0px 0px 0px 0px", .threshold = &.{0.0}, }; @@ -60,9 +60,9 @@ pub const IntersectionObserver = struct { } return .{ + .page = page, .callback = callback, .options = options, - .state = state, .observed_entries = .{}, }; } @@ -78,8 +78,8 @@ pub const IntersectionObserver = struct { } } - try self.observed_entries.append(self.state.arena, .{ - .state = self.state, + try self.observed_entries.append(self.page.arena, .{ + .page = self.page, .target = target_element, .options = &self.options, }); @@ -113,13 +113,13 @@ const IntersectionObserverOptions = struct { // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry // https://w3c.github.io/IntersectionObserver/#intersection-observer-entry pub const IntersectionObserverEntry = struct { - state: *SessionState, + page: *Page, target: *parser.Element, options: *IntersectionObserverOptions, // Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect(). pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect { - return Element._getBoundingClientRect(self.target, self.state); + return Element._getBoundingClientRect(self.target, self.page); } // Returns the ratio of the intersectionRect to the boundingClientRect. @@ -129,10 +129,14 @@ pub const IntersectionObserverEntry = struct { // Returns a DOMRectReadOnly representing the target's visible area. pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect { - return Element._getBoundingClientRect(self.target, self.state); + return Element._getBoundingClientRect(self.target, self.page); } - // A Boolean value which is true if the target element intersects with the intersection observer's root. If this is true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, then you know the transition is from intersecting to not-intersecting. + // A Boolean value which is true if the target element intersects with the + // intersection observer's root. If this is true, then, the + // IntersectionObserverEntry describes a transition into a state of + // intersection; if it's false, then you know the transition is from + // intersecting to not-intersecting. pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool { return true; } @@ -140,8 +144,8 @@ pub const IntersectionObserverEntry = struct { // Returns a DOMRectReadOnly for the intersection observer's root. pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect { const root = self.options.root.?; - if (@intFromPtr(root) == @intFromPtr(self.state.window.document)) { - return self.state.renderer.boundingRect(); + if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) { + return self.page.renderer.boundingRect(); } const root_type = try parser.nodeType(root); @@ -156,7 +160,7 @@ pub const IntersectionObserverEntry = struct { else => return error.InvalidState, } - return Element._getBoundingClientRect(element, self.state); + return Element._getBoundingClientRect(element, self.page); } // The Element whose intersection with the root changed. diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index e838394b..c4deff88 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator; const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Env = @import("../env.zig").Env; const NodeList = @import("nodelist.zig").NodeList; @@ -42,11 +42,11 @@ pub const MutationObserver = struct { // execute our callback with it. observed: std.ArrayListUnmanaged(*MutationRecord), - pub fn constructor(cbk: Env.Function, state: *SessionState) !MutationObserver { + pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver { return .{ .cbk = cbk, .observed = .{}, - .arena = state.arena, + .arena = page.arena, }; } diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index ea55fa5f..61cd1301 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -22,7 +22,7 @@ const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const generate = @import("../../runtime/generate.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const EventTarget = @import("event_target.zig").EventTarget; // DOM @@ -278,8 +278,8 @@ pub const Node = struct { return try parser.nodeHasChildNodes(self); } - pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList { - const allocator = state.arena; + pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList { + const allocator = page.arena; var list: NodeList = .{}; var n = try parser.nodeFirstChild(self) orelse return list; diff --git a/src/browser/dom/processing_instruction.zig b/src/browser/dom/processing_instruction.zig index ae4d93f6..8165e69d 100644 --- a/src/browser/dom/processing_instruction.zig +++ b/src/browser/dom/processing_instruction.zig @@ -20,7 +20,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const Node = @import("node.zig").Node; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; // https://dom.spec.whatwg.org/#processinginstruction pub const ProcessingInstruction = struct { @@ -39,9 +39,9 @@ pub const ProcessingInstruction = struct { // There's something wrong when we try to clone a ProcessInstruction normally. // The resulting object can't be cast back into a node (it crashes). This is // a simple workaround. - pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, state: *SessionState) !*parser.ProcessingInstruction { + pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, page: *Page) !*parser.ProcessingInstruction { return try parser.documentCreateProcessingInstruction( - @ptrCast(state.window.document), + @ptrCast(page.window.document), try get_target(self), (try get_data(self)) orelse "", ); diff --git a/src/browser/dom/text.zig b/src/browser/dom/text.zig index c12d3402..b1334db5 100644 --- a/src/browser/dom/text.zig +++ b/src/browser/dom/text.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const CharacterData = @import("character_data.zig").CharacterData; const CDATASection = @import("cdata_section.zig").CDATASection; @@ -32,9 +32,9 @@ pub const Text = struct { pub const prototype = *CharacterData; pub const subtype = .node; - pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text { + pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Text { return parser.documentCreateTextNode( - parser.documentHTMLToDocument(state.window.document), + parser.documentHTMLToDocument(page.window.document), data orelse "", ); } diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig index 826bb062..a79567b3 100644 --- a/src/browser/dom/tree_walker.zig +++ b/src/browser/dom/tree_walker.zig @@ -21,7 +21,7 @@ const parser = @import("../netsurf.zig"); const NodeFilter = @import("node_filter.zig").NodeFilter; const Env = @import("../env.zig").Env; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; // https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker pub const TreeWalker = struct { diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 57084dec..18923ed1 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -156,6 +156,9 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void { const testing = std.testing; test "dump.writeHTML" { + try parser.init(); + defer parser.deinit(); + try testWriteHTML( "
Over 9000!
", "
Over 9000!
", diff --git a/src/browser/env.zig b/src/browser/env.zig index b4b8d866..7d6627ec 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -1,29 +1,24 @@ const std = @import("std"); -const parser = @import("netsurf.zig"); -const URL = @import("../url.zig").URL; +const Page = @import("page.zig").Page; const js = @import("../runtime/js.zig"); -const storage = @import("storage/storage.zig"); const generate = @import("../runtime/generate.zig"); -const Renderer = @import("renderer.zig").Renderer; -const Loop = @import("../runtime/loop.zig").Loop; -const RequestFactory = @import("../http/client.zig").RequestFactory; const WebApis = struct { // Wrapped like this for debug ergonomics. // When we create our Env, a few lines down, we define it as: - // pub const Env = js.Env(*SessionState, WebApis); + // pub const Env = js.Env(*Page, WebApis); // // If there's a compile time error witht he Env, it's type will be readable, - // i.e.: runtime.js.Env(*browser.env.SessionState, browser.env.WebApis) + // i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis) // // But if we didn't wrap it in the struct, like we once didn't, and defined // env as: - // pub const Env = js.Env(*SessionState, Interfaces); + // pub const Env = js.Env(*Page, Interfaces); // // Because Interfaces is an anynoumous type, it doesn't have a friendly name // and errors would be something like: - // runtime.js.Env(*browser.env.SessionState, .{...A HUNDRED TYPES...}) + // runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...}) pub const Interfaces = generate.Tuple(.{ @import("crypto/crypto.zig").Crypto, @import("console/console.zig").Console, @@ -44,41 +39,5 @@ const WebApis = struct { pub const JsThis = Env.JsThis; pub const JsObject = Env.JsObject; pub const Function = Env.Function; -pub const Env = js.Env(*SessionState, WebApis); - -const Window = @import("html/window.zig").Window; -pub const Global = Window; - -pub const SessionState = struct { - loop: *Loop, - url: *const URL, - window: *Window, - renderer: *Renderer, - arena: std.mem.Allocator, - cookie_jar: *storage.CookieJar, - request_factory: RequestFactory, - - // dangerous, but set by the JS framework - // shorter-lived than the arena above, which - // exists for the entire rendering of the page - call_arena: std.mem.Allocator = undefined, - - pub fn getOrCreateNodeWrapper(self: *SessionState, comptime T: type, node: *parser.Node) !*T { - if (try self.getNodeWrapper(T, node)) |wrap| { - return wrap; - } - - const wrap = try self.arena.create(T); - wrap.* = T{}; - - parser.nodeSetEmbedderData(node, wrap); - return wrap; - } - - pub fn getNodeWrapper(_: *SessionState, comptime T: type, node: *parser.Node) !?*T { - if (parser.nodeGetEmbedderData(node)) |wrap| { - return @alignCast(@ptrCast(wrap)); - } - return null; - } -}; +pub const Env = js.Env(*Page, WebApis); +pub const Global = @import("html/window.zig").Window; diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index a677bd01..f0b533d4 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Window = @import("window.zig").Window; const Element = @import("../dom/element.zig").Element; @@ -86,18 +86,18 @@ pub const HTMLDocument = struct { } } - pub fn get_cookie(_: *parser.DocumentHTML, state: *SessionState) ![]const u8 { + pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 { var buf: std.ArrayListUnmanaged(u8) = .{}; - try state.cookie_jar.forRequest(&state.url.uri, buf.writer(state.arena), .{ .navigation = true }); + try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true }); return buf.items; } - pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, state: *SessionState) ![]const u8 { + pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 { // we use the cookie jar's allocator to parse the cookie because it // outlives the page's arena. - const c = try Cookie.parse(state.cookie_jar.allocator, &state.url.uri, cookie_str); + const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str); errdefer c.deinit(); - try state.cookie_jar.add(c, std.time.timestamp()); + try page.cookie_jar.add(c, std.time.timestamp()); return cookie_str; } @@ -110,8 +110,8 @@ pub const HTMLDocument = struct { return v; } - pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList { - const arena = state.arena; + pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList { + const arena = page.arena; var list: NodeList = .{}; if (name.len == 0) return list; @@ -130,24 +130,24 @@ pub const HTMLDocument = struct { return list; } - pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false); + pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false); } - pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false); + pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false); } - pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { - return get_embeds(self, state); + pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { + return get_embeds(self, page); } - pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false); + pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false); } - pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false); + pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false); } pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection { @@ -182,12 +182,12 @@ pub const HTMLDocument = struct { return "off"; } - pub fn get_defaultView(_: *parser.DocumentHTML, state: *const SessionState) *Window { - return state.window; + pub fn get_defaultView(_: *parser.DocumentHTML, page: *Page) *Window { + return &page.window; } - pub fn get_readyState(node: *parser.DocumentHTML, state: *SessionState) ![]const u8 { - const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node)); + pub fn get_readyState(node: *parser.DocumentHTML, page: *Page) ![]const u8 { + const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node)); return @tagName(self.ready_state); } @@ -232,41 +232,41 @@ pub const HTMLDocument = struct { // Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment // This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported. // This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet. - pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) !?ElementUnion { + pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) !?ElementUnion { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); - const element = state.renderer.getElementAtPosition(ix, iy) orelse return null; + const element = page.renderer.getElementAtPosition(ix, iy) orelse return null; // TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?) return try Element.toInterface(element); } // Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport. - pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, state: *SessionState) ![]ElementUnion { + pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) ![]ElementUnion { const ix: i32 = @intFromFloat(@floor(x)); const iy: i32 = @intFromFloat(@floor(y)); - const element = state.renderer.getElementAtPosition(ix, iy) orelse return &.{}; + const element = page.renderer.getElementAtPosition(ix, iy) orelse return &.{}; // TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?) var list: std.ArrayListUnmanaged(ElementUnion) = .empty; - try list.ensureTotalCapacity(state.call_arena, 3); + try list.ensureTotalCapacity(page.call_arena, 3); list.appendAssumeCapacity(try Element.toInterface(element)); // Since we are using a flat renderer there is no hierarchy of elements. What we do know is that the element is part of the main document. // Thus we can add the HtmlHtmlElement and it's child HTMLBodyElement to the returned list. // TBD Should we instead return every parent that is an element? Note that a child does not physically need to be overlapping the parent. // Should we do a render pass on demand? - const doc_elem = try parser.documentGetDocumentElement(parser.documentHTMLToDocument(state.window.document)) orelse { + const doc_elem = try parser.documentGetDocumentElement(parser.documentHTMLToDocument(page.window.document)) orelse { return list.items; }; - if (try parser.documentHTMLBody(state.window.document)) |body| { + if (try parser.documentHTMLBody(page.window.document)) |body| { list.appendAssumeCapacity(try Element.toInterface(parser.bodyToElement(body))); } list.appendAssumeCapacity(try Element.toInterface(doc_elem)); return list.items; } - pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, state: *SessionState) !void { - const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); + pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, page: *Page) !void { + const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); self.ready_state = .interactive; const evt = try parser.eventCreate(); @@ -276,8 +276,8 @@ pub const HTMLDocument = struct { _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt); } - pub fn documentIsComplete(html_doc: *parser.DocumentHTML, state: *SessionState) !void { - const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); + pub fn documentIsComplete(html_doc: *parser.DocumentHTML, page: *Page) !void { + const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); self.ready_state = .complete; } }; @@ -383,12 +383,12 @@ test "Browser.HTML.Document" { .{ "document.readyState", "loading" }, }, .{}); - try HTMLDocument.documentIsLoaded(runner.window.document, &runner.state); + try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page); try runner.testCases(&.{ .{ "document.readyState", "interactive" }, }, .{}); - try HTMLDocument.documentIsComplete(runner.window.document, &runner.state); + try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page); try runner.testCases(&.{ .{ "document.readyState", "complete" }, }, .{}); diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index d93ba2b4..0f4b16aa 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const generate = @import("../../runtime/generate.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; @@ -111,8 +111,8 @@ pub const HTMLElement = struct { style: CSSStyleDeclaration = .empty, - pub fn get_style(e: *parser.ElementHTML, state: *SessionState) !*CSSStyleDeclaration { - const self = try state.getOrCreateNodeWrapper(HTMLElement, @ptrCast(e)); + pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration { + const self = try page.getOrCreateNodeWrapper(HTMLElement, @ptrCast(e)); return &self.style; } @@ -228,26 +228,26 @@ pub const HTMLAnchorElement = struct { return try parser.nodeSetTextContent(parser.anchorToNode(self), v); } - inline fn url(self: *parser.Anchor, state: *SessionState) !URL { + inline fn url(self: *parser.Anchor, page: *Page) !URL { const href = try parser.anchorGetHref(self); - return URL.constructor(href, null, state); // TODO inject base url + return URL.constructor(href, null, page); // TODO inject base url } // TODO return a disposable string - pub fn get_origin(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try u.get_origin(state); + pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try u.get_origin(page); } // TODO return a disposable string - pub fn get_protocol(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return u.get_protocol(state); + pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return u.get_protocol(page); } - pub fn set_protocol(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); u.uri.scheme = v; const href = try u.toString(arena); @@ -255,12 +255,12 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_host(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try u.get_host(state); + pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try u.get_host(page); } - pub fn set_host(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { + pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { // search : separator var p: ?u16 = null; var h: []const u8 = undefined; @@ -272,8 +272,8 @@ pub const HTMLAnchorElement = struct { } } - const arena = state.arena; - var u = try url(self, state); + const arena = page.arena; + var u = try url(self, page); if (p) |pp| { u.uri.host = .{ .raw = h }; @@ -288,28 +288,28 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_hostname(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try state.arena.dupe(u8, u.get_hostname()); + pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try page.arena.dupe(u8, u.get_hostname()); } - pub fn set_hostname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); u.uri.host = .{ .raw = v }; const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_port(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try u.get_port(state); + pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try u.get_port(page); } - pub fn set_port(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); if (v != null and v.?.len > 0) { u.uri.port = try std.fmt.parseInt(u16, v.?, 10); @@ -322,14 +322,14 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_username(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try state.arena.dupe(u8, u.get_username()); + pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try page.arena.dupe(u8, u.get_username()); } - pub fn set_username(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); if (v) |vv| { u.uri.user = .{ .raw = vv }; @@ -342,14 +342,14 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_password(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try state.arena.dupe(u8, u.get_password()); + pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try page.arena.dupe(u8, u.get_password()); } - pub fn set_password(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); if (v) |vv| { u.uri.password = .{ .raw = vv }; @@ -362,14 +362,14 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_pathname(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try state.arena.dupe(u8, u.get_pathname()); + pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try page.arena.dupe(u8, u.get_pathname()); } - pub fn set_pathname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); u.uri.path = .{ .raw = v }; const href = try u.toString(arena); @@ -377,14 +377,14 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_search(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try u.get_search(state); + pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try u.get_search(page); } - pub fn set_search(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); if (v) |vv| { u.uri.query = .{ .raw = vv }; @@ -397,14 +397,14 @@ pub const HTMLAnchorElement = struct { } // TODO return a disposable string - pub fn get_hash(self: *parser.Anchor, state: *SessionState) ![]const u8 { - var u = try url(self, state); - return try u.get_hash(state); + pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { + var u = try url(self, page); + return try u.get_hash(page); } - pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { - const arena = state.arena; - var u = try url(self, state); + pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + const arena = page.arena; + var u = try url(self, page); if (v) |vv| { u.uri.fragment = .{ .raw = vv }; @@ -567,8 +567,8 @@ pub const HTMLImageElement = struct { pub const subtype = .node; pub const js_name = "Image"; - pub fn constructor(width: ?u32, height: ?u32, state: *const SessionState) !*parser.Image { - const element = try parser.documentCreateElement(parser.documentHTMLToDocument(state.window.document), "img"); + pub fn constructor(width: ?u32, height: ?u32, page: *const Page) !*parser.Image { + const element = try parser.documentCreateElement(parser.documentHTMLToDocument(page.window.document), "img"); const image: *parser.Image = @ptrCast(element); if (width) |width_| try parser.imageSetWidth(image, width_); if (height) |height_| try parser.imageSetHeight(image, height_); diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 52968d13..604b9622 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -16,7 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const URL = @import("../url/url.zig").URL; @@ -24,18 +24,18 @@ const URL = @import("../url/url.zig").URL; pub const Location = struct { url: ?URL = null, - pub fn get_href(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_href(state); + pub fn get_href(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_href(page); return ""; } - pub fn get_protocol(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_protocol(state); + pub fn get_protocol(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_protocol(page); return ""; } - pub fn get_host(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_host(state); + pub fn get_host(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_host(page); return ""; } @@ -44,8 +44,8 @@ pub const Location = struct { return ""; } - pub fn get_port(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_port(state); + pub fn get_port(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_port(page); return ""; } @@ -54,18 +54,18 @@ pub const Location = struct { return ""; } - pub fn get_search(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_search(state); + pub fn get_search(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_search(page); return ""; } - pub fn get_hash(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_hash(state); + pub fn get_hash(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_hash(page); return ""; } - pub fn get_origin(self: *Location, state: *SessionState) ![]const u8 { - if (self.url) |*u| return u.get_origin(state); + pub fn get_origin(self: *Location, page: *Page) ![]const u8 { + if (self.url) |*u| return u.get_origin(page); return ""; } @@ -82,8 +82,8 @@ pub const Location = struct { // TODO pub fn _reload(_: *Location) !void {} - pub fn _toString(self: *Location, state: *SessionState) ![]const u8 { - return try self.get_href(state); + pub fn _toString(self: *Location, page: *Page) ![]const u8 { + return try self.get_href(page); } }; diff --git a/src/browser/html/select.zig b/src/browser/html/select.zig index f07e5d64..5a73138f 100644 --- a/src/browser/html/select.zig +++ b/src/browser/html/select.zig @@ -19,7 +19,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const HTMLElement = @import("elements.zig").HTMLElement; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; pub const HTMLSelectElement = struct { pub const Self = parser.Select; @@ -69,8 +69,8 @@ pub const HTMLSelectElement = struct { return parser.selectSetMultiple(select, multiple); } - pub fn get_selectedIndex(select: *parser.Select, state: *SessionState) !i32 { - const self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); + pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 { + const self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); const selected_index = try parser.selectGetSelectedIndex(select); // See the explicit_index_set field documentation @@ -88,8 +88,8 @@ 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, state: *SessionState) !void { - var self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); + pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void { + var self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); self.explicit_index_set = true; const options = try parser.selectGetOptions(select); diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 164e220a..b58f7cbc 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -21,7 +21,7 @@ const std = @import("std"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const Function = @import("../env.zig").Function; -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const Loop = @import("../../runtime/loop.zig").Loop; const Navigator = @import("navigator.zig").Navigator; @@ -130,15 +130,15 @@ pub const Window = struct { } // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. - pub fn get_innerHeight(_: *Window, state: *SessionState) u32 { + pub fn get_innerHeight(_: *Window, page: *Page) u32 { // We do not have scrollbars or padding so this is the same as Element.clientHeight - return state.renderer.height(); + return page.renderer.height(); } // The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present. - pub fn get_innerWidth(_: *Window, state: *SessionState) u32 { + pub fn get_innerWidth(_: *Window, page: *Page) u32 { // We do not have scrollbars or padding so this is the same as Element.clientWidth - return state.renderer.width(); + return page.renderer.width(); } pub fn get_name(self: *Window) []const u8 { @@ -182,40 +182,40 @@ pub const Window = struct { } // TODO handle callback arguments. - pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, state: *SessionState) !u32 { - return self.createTimeout(cbk, delay, state, false); + pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 { + return self.createTimeout(cbk, delay, page, false); } // TODO handle callback arguments. - pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, state: *SessionState) !u32 { - return self.createTimeout(cbk, delay, state, true); + pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 { + return self.createTimeout(cbk, delay, page, true); } - pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void { + pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void { const kv = self.timers.fetchRemove(id) orelse return; - try state.loop.cancel(kv.value.loop_id); + try page.loop.cancel(kv.value.loop_id); } - pub fn _clearInterval(self: *Window, id: u32, state: *SessionState) !void { + pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void { const kv = self.timers.fetchRemove(id) orelse return; - try state.loop.cancel(kv.value.loop_id); + try page.loop.cancel(kv.value.loop_id); } - pub fn _matchMedia(_: *const Window, media: []const u8, state: *SessionState) !MediaQueryList { + pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList { return .{ .matches = false, // TODO? - .media = try state.arena.dupe(u8, media), + .media = try page.arena.dupe(u8, media), }; } - fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { + fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, comptime repeat: bool) !u32 { if (self.timers.count() > 512) { return error.TooManyTimeout; } const timer_id = self.timer_id +% 1; self.timer_id = timer_id; - const arena = state.arena; + const arena = page.arena; const gop = try self.timers.getOrPut(arena, timer_id); if (gop.found_existing) { @@ -235,7 +235,7 @@ pub const Window = struct { .node = .{ .func = TimerCallback.run }, .repeat = if (repeat) delay else null, }; - callback.loop_id = try state.loop.timeout(delay, &callback.node); + callback.loop_id = try page.loop.timeout(delay, &callback.node); gop.value_ptr.* = callback; return timer_id; diff --git a/src/browser/page.zig b/src/browser/page.zig index ed2a2cbd..e5ac0d38 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -22,16 +22,16 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const Dump = @import("dump.zig"); +const Env = @import("env.zig").Env; const Mime = @import("mime.zig").Mime; const DataURI = @import("datauri.zig").DataURI; const Session = @import("session.zig").Session; const Renderer = @import("renderer.zig").Renderer; -const SessionState = @import("env.zig").SessionState; const Window = @import("html/window.zig").Window; const Walker = @import("dom/walker.zig").WalkerDepthFirst; -const Env = @import("env.zig").Env; const Loop = @import("../runtime/loop.zig").Loop; const HTMLDocument = @import("html/document.zig").HTMLDocument; +const RequestFactory = @import("../http/client.zig").RequestFactory; const URL = @import("../url.zig").URL; @@ -48,13 +48,22 @@ const polyfill = @import("polyfill/polyfill.zig"); // The page handle all its memory in an arena allocator. The arena is reseted // when end() is called. pub const Page = struct { + // Our event loop + loop: *Loop, + + cookie_jar: *storage.CookieJar, + + // Pre-configured http/cilent.zig used to make HTTP requests. + request_factory: RequestFactory, + session: *Session, - // an arena with a lifetime for the entire duration of the page + // An arena with a lifetime for the entire duration of the page arena: Allocator, - // Gets injected into any WebAPI method that needs it - state: SessionState, + // Managed by the JS runtime, meant to have a much shorter life than the + // above arena. It should only be used by WebAPIs. + call_arena: Allocator, // Serves are the root object of our JavaScript environment window: Window, @@ -62,6 +71,8 @@ pub const Page = struct { // The URL of the page url: URL, + // If the body of the main page isn't HTML, we capture its raw bytes here + // (currently, this is only useful in fetch mode with the --dump option) raw_data: ?[]const u8, renderer: Renderer, @@ -70,6 +81,8 @@ pub const Page = struct { window_clicked_event_node: parser.EventNode, + // Our JavaScript context for this specific page. This is what we use to + // execute any JavaScript scope: *Env.Scope, // List of modules currently fetched/loaded. @@ -87,21 +100,17 @@ pub const Page = struct { .raw_data = null, .url = URL.empty, .session = session, + .call_arena = undefined, + .loop = browser.app.loop, .renderer = Renderer.init(arena), + .cookie_jar = &session.cookie_jar, .microtask_node = .{ .func = microtaskCallback }, .window_clicked_event_node = .{ .func = windowClicked }, - .state = .{ - .arena = arena, - .url = &self.url, - .window = &self.window, - .renderer = &self.renderer, - .loop = browser.app.loop, - .cookie_jar = &session.cookie_jar, - .request_factory = browser.http_client.requestFactory(browser.notification), - }, - .scope = try session.executor.startScope(&self.window, &self.state, self, true), + .request_factory = browser.http_client.requestFactory(browser.notification), + .scope = undefined, .module_map = .empty, }; + self.scope = try session.executor.startScope(&self.window, self, self, true); // load polyfills try polyfill.load(self.arena, self.scope); @@ -180,7 +189,7 @@ pub const Page = struct { try self.loadHTMLDoc(fbs.reader(), "utf-8"); // We do not processHTMLDoc here as we know we don't have any scripts // This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented - try HTMLDocument.documentIsComplete(self.window.document, &self.state); + try HTMLDocument.documentIsComplete(self.window.document, self); return; } @@ -243,7 +252,7 @@ pub const Page = struct { } // https://html.spec.whatwg.org/#read-html - fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void { + pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void { const ccharset = try self.arena.dupeZ(u8, charset); const html_doc = try parser.documentHTMLParse(reader, ccharset); @@ -352,7 +361,7 @@ pub const Page = struct { // at the point where all subresources apart from async script elements // have loaded. // https://html.spec.whatwg.org/#reporting-document-loading-status - try HTMLDocument.documentIsLoaded(html_doc, &self.state); + try HTMLDocument.documentIsLoaded(html_doc, self); // eval async scripts. for (async_scripts.items) |script| { @@ -363,7 +372,7 @@ pub const Page = struct { try parser.documentHTMLSetCurrentScript(html_doc, null); } - try HTMLDocument.documentIsComplete(html_doc, &self.state); + try HTMLDocument.documentIsComplete(html_doc, self); // dispatch window.load event const loadevt = try parser.eventCreate(); @@ -470,7 +479,7 @@ pub const Page = struct { errdefer request.deinit(); var arr: std.ArrayListUnmanaged(u8) = .{}; - try self.state.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts); + try self.cookie_jar.forRequest(&url.uri, arr.writer(self.arena), opts); if (arr.items.len > 0) { try request.addHeader("Cookie", arr.items, .{}); @@ -534,126 +543,145 @@ pub const Page = struct { .session = self.session, .href = try arena.dupe(u8, href), }; - _ = try self.state.loop.timeout(0, &navi.navigate_node); + _ = try self.loop.timeout(0, &navi.navigate_node); }, else => {}, } } - const DelayedNavigation = struct { - navigate_node: Loop.CallbackNode = .{ .func = DelayedNavigation.delayNavigate }, - session: *Session, - href: []const u8, - - fn delayNavigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void { - _ = repeat_delay; - const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node); - self.session.pageNavigate(self.href) catch |err| { - // TODO: should we trigger a specific event here? - log.err(.page, "delayed navigation error", .{ .err = err }); - }; + pub fn getOrCreateNodeWrapper(self: *Page, comptime T: type, node: *parser.Node) !*T { + if (try self.getNodeWrapper(T, node)) |wrap| { + return wrap; } + + const wrap = try self.arena.create(T); + wrap.* = T{}; + + parser.nodeSetEmbedderData(node, wrap); + return wrap; + } + + pub fn getNodeWrapper(_: *Page, comptime T: type, node: *parser.Node) !?*T { + if (parser.nodeGetEmbedderData(node)) |wrap| { + return @alignCast(@ptrCast(wrap)); + } + return null; + } +}; + +const DelayedNavigation = struct { + navigate_node: Loop.CallbackNode = .{ .func = DelayedNavigation.delay_navigate }, + session: *Session, + href: []const u8, + + fn delay_navigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void { + _ = repeat_delay; + const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node); + self.session.pageNavigate(self.href) catch |err| { + // TODO: should we trigger a specific event here? + log.err(.page, "delayed navigation error", .{ .err = err }); + }; + } +}; + +const Script = struct { + kind: Kind, + is_async: bool, + is_defer: bool, + src: ?[]const u8, + element: *parser.Element, + // The javascript to load after we successfully load the script + onload: ?[]const u8, + + // The javascript to load if we have an error executing the script + // For now, we ignore this, since we still have a lot of errors that we + // shouldn't + //onerror: ?[]const u8, + + const Kind = enum { + module, + javascript, }; - const Script = struct { - kind: Kind, - is_async: bool, - is_defer: bool, - src: ?[]const u8, - element: *parser.Element, - // The javascript to load after we successfully load the script - onload: ?[]const u8, - - // The javascript to load if we have an error executing the script - // For now, we ignore this, since we still have a lot of errors that we - // shouldn't - //onerror: ?[]const u8, - - const Kind = enum { - module, - javascript, - }; - - fn init(e: *parser.Element) !?Script { - // ignore non-script tags - const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e))); - if (tag != .script) { - return null; - } - - if (try parser.elementGetAttribute(e, "nomodule") != null) { - // these scripts should only be loaded if we don't support modules - // but since we do support modules, we can just skip them. - return null; - } - - const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse { - return null; - }; - - return .{ - .kind = kind, - .element = e, - .src = try parser.elementGetAttribute(e, "src"), - .onload = try parser.elementGetAttribute(e, "onload"), - .is_async = try parser.elementGetAttribute(e, "async") != null, - .is_defer = try parser.elementGetAttribute(e, "defer") != null, - }; - } - - // > type - // > Attribute is not set (default), an empty string, or a JavaScript MIME - // > type indicates that the script is a "classic script", containing - // > JavaScript code. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type - fn parseKind(script_type_: ?[]const u8) ?Kind { - const script_type = script_type_ orelse return .javascript; - if (script_type.len == 0) { - return .javascript; - } - - if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript; - if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript; - if (std.mem.eql(u8, script_type, "module")) return .module; - + fn init(e: *parser.Element) !?Script { + // ignore non-script tags + const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e))); + if (tag != .script) { return null; } - fn eval(self: *const Script, page: *Page, body: []const u8) !void { - var try_catch: Env.TryCatch = undefined; - try_catch.init(page.scope); - defer try_catch.deinit(); + if (try parser.elementGetAttribute(e, "nomodule") != null) { + // these scripts should only be loaded if we don't support modules + // but since we do support modules, we can just skip them. + return null; + } - const src = self.src orelse "inline"; - const res = switch (self.kind) { - .javascript => page.scope.exec(body, src), - .module => blk: { - switch (try page.scope.module(body, src)) { - .value => |v| break :blk v, - .exception => |e| { - log.warn(.page, "eval module", .{ .src = src, .err = try e.exception(page.arena) }); - return error.JsErr; - }, - } - }, - } catch { + const kind = parseKind(try parser.elementGetAttribute(e, "type")) orelse { + return null; + }; + + return .{ + .kind = kind, + .element = e, + .src = try parser.elementGetAttribute(e, "src"), + .onload = try parser.elementGetAttribute(e, "onload"), + .is_async = try parser.elementGetAttribute(e, "async") != null, + .is_defer = try parser.elementGetAttribute(e, "defer") != null, + }; + } + + // > type + // > Attribute is not set (default), an empty string, or a JavaScript MIME + // > type indicates that the script is a "classic script", containing + // > JavaScript code. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type + fn parseKind(script_type_: ?[]const u8) ?Kind { + const script_type = script_type_ orelse return .javascript; + if (script_type.len == 0) { + return .javascript; + } + + if (std.mem.eql(u8, script_type, "application/javascript")) return .javascript; + if (std.mem.eql(u8, script_type, "text/javascript")) return .javascript; + if (std.mem.eql(u8, script_type, "module")) return .module; + + return null; + } + + fn eval(self: *const Script, page: *Page, body: []const u8) !void { + var try_catch: Env.TryCatch = undefined; + try_catch.init(page.scope); + defer try_catch.deinit(); + + const src = self.src orelse "inline"; + const res = switch (self.kind) { + .javascript => page.scope.exec(body, src), + .module => blk: { + switch (try page.scope.module(body, src)) { + .value => |v| break :blk v, + .exception => |e| { + log.warn(.page, "eval module", .{ .src = src, .err = try e.exception(page.arena) }); + return error.JsErr; + }, + } + }, + } catch { + if (try try_catch.err(page.arena)) |msg| { + log.warn(.page, "eval script", .{ .src = src, .err = msg }); + } + return error.JsErr; + }; + _ = res; + + if (self.onload) |onload| { + _ = page.scope.exec(onload, "script_on_load") catch { if (try try_catch.err(page.arena)) |msg| { - log.warn(.page, "eval script", .{ .src = src, .err = msg }); + log.warn(.page, "eval onload", .{ .src = src, .err = msg }); } return error.JsErr; }; - _ = res; - - if (self.onload) |onload| { - _ = page.scope.exec(onload, "script_on_load") catch { - if (try try_catch.err(page.arena)) |msg| { - log.warn(.page, "eval onload", .{ .src = src, .err = msg }); - } - return error.JsErr; - }; - } } - }; + } }; pub const NavigateReason = enum { diff --git a/src/browser/polyfill/fetch.zig b/src/browser/polyfill/fetch.zig index a0166bdd..76d7cc58 100644 --- a/src/browser/polyfill/fetch.zig +++ b/src/browser/polyfill/fetch.zig @@ -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.scope); + try @import("polyfill.zig").load(testing.allocator, runner.page.scope); try runner.testCases(&.{ .{ @@ -31,16 +31,16 @@ test "Browser.fetch" { .{ "ok", "true" }, }, .{}); - try runner.testCases(&.{ - .{ - \\ var ok2 = false; - \\ const request2 = new Request("http://127.0.0.1:9582/loader"); - \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); - \\ false; - , - "false", - }, - // all events have been resolved. - .{ "ok2", "true" }, - }, .{}); + // try runner.testCases(&.{ + // .{ + // \\ var ok2 = false; + // \\ const request2 = new Request("http://127.0.0.1:9582/loader"); + // \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); + // \\ false; + // , + // "false", + // }, + // // all events have been resolved. + // .{ "ok2", "true" }, + // }, .{}); } diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index a25935ad..89e41ed4 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -231,7 +231,7 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool { fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool { const origin_uri = origin_uri_ orelse return true; - const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; + const origin_host = (origin_uri.host orelse return error.InvalidURI333).percent_encoded; // common case if (std.mem.eql(u8, target_host, origin_host)) { diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 3bf496bd..fd790380 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const query = @import("query.zig"); @@ -47,9 +47,9 @@ pub const URL = struct { pub fn constructor( url: []const u8, base: ?[]const u8, - state: *SessionState, + page: *Page, ) !URL { - const arena = state.arena; + const arena = page.arena; const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" }); const uri = std.Uri.parse(raw) catch return error.TypeError; @@ -66,8 +66,8 @@ pub const URL = struct { }; } - pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 { - var buf = std.ArrayList(u8).init(state.arena); + pub fn get_origin(self: *URL, page: *Page) ![]const u8 { + var buf = std.ArrayList(u8).init(page.arena); try self.uri.writeToStream(.{ .scheme = true, .authentication = false, @@ -82,8 +82,8 @@ pub const URL = struct { // get_href returns the URL by writing all its components. // The query is replaced by a dump of search params. // - pub fn get_href(self: *URL, state: *SessionState) ![]const u8 { - const arena = state.arena; + pub fn get_href(self: *URL, page: *Page) ![]const u8 { + const arena = page.arena; // retrieve the query search from search_params. const cur = self.uri.query; defer self.uri.query = cur; @@ -109,8 +109,8 @@ pub const URL = struct { return buf.items; } - pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 { - return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" }); + pub fn get_protocol(self: *URL, page: *Page) ![]const u8 { + return try std.mem.concat(page.arena, u8, &[_][]const u8{ self.uri.scheme, ":" }); } pub fn get_username(self: *URL) []const u8 { @@ -121,8 +121,8 @@ pub const URL = struct { return uriComponentNullStr(self.uri.password); } - pub fn get_host(self: *URL, state: *SessionState) ![]const u8 { - var buf = std.ArrayList(u8).init(state.arena); + pub fn get_host(self: *URL, page: *Page) ![]const u8 { + var buf = std.ArrayList(u8).init(page.arena); try self.uri.writeToStream(.{ .scheme = false, @@ -139,8 +139,8 @@ pub const URL = struct { return uriComponentNullStr(self.uri.host); } - pub fn get_port(self: *URL, state: *SessionState) ![]const u8 { - const arena = state.arena; + pub fn get_port(self: *URL, page: *Page) ![]const u8 { + const arena = page.arena; if (self.uri.port == null) return try arena.dupe(u8, ""); var buf = std.ArrayList(u8).init(arena); @@ -153,8 +153,8 @@ pub const URL = struct { return uriComponentStr(self.uri.path); } - pub fn get_search(self: *URL, state: *SessionState) ![]const u8 { - const arena = state.arena; + pub fn get_search(self: *URL, page: *Page) ![]const u8 { + const arena = page.arena; if (self.search_params.get_size() == 0) return try arena.dupe(u8, ""); var buf: std.ArrayListUnmanaged(u8) = .{}; @@ -164,8 +164,8 @@ pub const URL = struct { return buf.items; } - pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 { - const arena = state.arena; + pub fn get_hash(self: *URL, page: *Page) ![]const u8 { + const arena = page.arena; if (self.uri.fragment == null) return try arena.dupe(u8, ""); return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) }); @@ -175,8 +175,8 @@ pub const URL = struct { return &self.search_params; } - pub fn _toJSON(self: *URL, state: *SessionState) ![]const u8 { - return try self.get_href(state); + pub fn _toJSON(self: *URL, page: *Page) ![]const u8 { + return try self.get_href(page); } }; @@ -200,8 +200,8 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { pub const URLSearchParams = struct { values: query.Values, - pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams { - return init(state.arena, qs); + pub fn constructor(qs: ?[]const u8, page: *Page) !URLSearchParams { + return init(page.arena, qs); } pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams { diff --git a/src/browser/xhr/event_target.zig b/src/browser/xhr/event_target.zig index cb6e36e0..fe686973 100644 --- a/src/browser/xhr/event_target.zig +++ b/src/browser/xhr/event_target.zig @@ -25,7 +25,7 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventHandler = @import("../events/event.zig").EventHandler; const parser = @import("../netsurf.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; pub const XMLHttpRequestEventTarget = struct { pub const prototype = *EventTarget; @@ -86,34 +86,34 @@ pub const XMLHttpRequestEventTarget = struct { return self.onloadend_cbk; } - pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id); - try self.register(state.arena, "loadstart", handler); + try self.register(page.arena, "loadstart", handler); self.onloadstart_cbk = handler; } - pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id); - try self.register(state.arena, "progress", handler); + try self.register(page.arena, "progress", handler); self.onprogress_cbk = handler; } - pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id); - try self.register(state.arena, "abort", handler); + try self.register(page.arena, "abort", handler); self.onabort_cbk = handler; } - pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id); - try self.register(state.arena, "load", handler); + try self.register(page.arena, "load", handler); self.onload_cbk = handler; } - pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id); - try self.register(state.arena, "timeout", handler); + try self.register(page.arena, "timeout", handler); self.ontimeout_cbk = handler; } - pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void { + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Function, page: *Page) !void { if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id); - try self.register(state.arena, "loadend", handler); + try self.register(page.arena, "loadend", handler); self.onloadend_cbk = handler; } }; diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index ae8a3023..afa2e699 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -23,7 +23,7 @@ const Allocator = std.mem.Allocator; const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const iterator = @import("../iterator/iterator.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; pub const Interfaces = .{ FormData, @@ -53,19 +53,19 @@ pub const Interfaces = .{ pub const FormData = struct { entries: std.ArrayListUnmanaged(Entry), - pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !FormData { + pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData { const form = form_ orelse return .{ .entries = .empty }; - return fromForm(form, submitter_, state, .{}); + return fromForm(form, submitter_, page, .{}); } const FromFormOpts = struct { - // Uses the state.arena if null. This is needed for when we're handling + // Uses the page.arena if null. This is needed for when we're handling // form submission from the Page, and we want to capture the form within // the session's transfer_arena. allocator: ?Allocator = null, }; - pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState, opts: FromFormOpts) !FormData { - const entries = try collectForm(opts.allocator orelse state.arena, form, submitter_, state); + pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page, opts: FromFormOpts) !FormData { + const entries = try collectForm(opts.allocator orelse page.arena, form, submitter_, page); return .{ .entries = entries }; } @@ -74,8 +74,8 @@ pub const FormData = struct { return result.entry.value; } - pub fn _getAll(self: *const FormData, key: []const u8, state: *SessionState) ![][]const u8 { - const arena = state.call_arena; + pub fn _getAll(self: *const FormData, key: []const u8, page: *Page) ![][]const u8 { + const arena = page.call_arena; var arr: std.ArrayListUnmanaged([]const u8) = .empty; for (self.entries.items) |entry| { if (std.mem.eql(u8, key, entry.key)) { @@ -91,15 +91,15 @@ pub const FormData = struct { // TODO: value should be a string or blog // TODO: another optional parameter for the filename - pub fn _set(self: *FormData, key: []const u8, value: []const u8, state: *SessionState) !void { + pub fn _set(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { self._delete(key); - return self._append(key, value, state); + return self._append(key, value, page); } // TODO: value should be a string or blog // TODO: another optional parameter for the filename - pub fn _append(self: *FormData, key: []const u8, value: []const u8, state: *SessionState) !void { - const arena = state.arena; + pub fn _append(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { + const arena = page.arena; return self.entries.append(arena, .{ .key = try arena.dupe(u8, key), .value = try arena.dupe(u8, value) }); } @@ -198,7 +198,7 @@ const EntryIterator = struct { } }; -fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !std.ArrayListUnmanaged(Entry) { +fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !std.ArrayListUnmanaged(Entry) { const collection = try parser.formGetCollection(form); const len = try parser.htmlCollectionGetLength(collection); @@ -252,7 +252,7 @@ fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.Elemen }, .select => { const select: *parser.Select = @ptrCast(node); - try collectSelectValues(arena, select, name, &entries, state); + try collectSelectValues(arena, select, name, &entries, page); }, .textarea => { const textarea: *parser.TextArea = @ptrCast(node); @@ -275,12 +275,12 @@ fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.Elemen return entries; } -fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *std.ArrayListUnmanaged(Entry), state: *SessionState) !void { +fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *std.ArrayListUnmanaged(Entry), page: *Page) !void { const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement; // Go through the HTMLSelectElement because it has specific logic for handling // the default selected option, which libdom doesn't properly handle - const selected_index = try HTMLSelectElement.get_selectedIndex(select, state); + const selected_index = try HTMLSelectElement.get_selectedIndex(select, page); if (selected_index == -1) { return; } diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 9dc71036..eb71b5fa 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -29,7 +29,7 @@ const URL = @import("../../url.zig").URL; const Mime = @import("../mime.zig").Mime; const parser = @import("../netsurf.zig"); const http = @import("../../http/client.zig"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const CookieJar = @import("../storage/storage.zig").CookieJar; // XHR interfaces @@ -238,8 +238,8 @@ pub const XMLHttpRequest = struct { const min_delay: u64 = 50000000; // 50ms - pub fn constructor(session_state: *SessionState) !XMLHttpRequest { - const arena = session_state.arena; + pub fn constructor(page: *Page) !XMLHttpRequest { + const arena = page.arena; return .{ .arena = arena, .headers = Headers.init(arena), @@ -247,8 +247,8 @@ pub const XMLHttpRequest = struct { .method = undefined, .state = .unsent, .url = null, - .origin_url = session_state.url, - .cookie_jar = session_state.cookie_jar, + .origin_url = &page.url, + .cookie_jar = page.cookie_jar, }; } @@ -408,7 +408,7 @@ pub const XMLHttpRequest = struct { } // TODO body can be either a XMLHttpRequestBodyInit or a document - pub fn _send(self: *XMLHttpRequest, body: ?[]const u8, session_state: *SessionState) !void { + pub fn _send(self: *XMLHttpRequest, body: ?[]const u8, page: *Page) !void { if (self.state != .opened) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState; @@ -416,7 +416,7 @@ pub const XMLHttpRequest = struct { self.send_flag = true; - self.request = try session_state.request_factory.create(self.method, &self.url.?.uri); + self.request = try page.request_factory.create(self.method, &self.url.?.uri); var request = &self.request.?; errdefer request.deinit(); @@ -426,7 +426,7 @@ pub const XMLHttpRequest = struct { { var arr: std.ArrayListUnmanaged(u8) = .{}; - try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(session_state.arena), .{ + try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(page.arena), .{ .navigation = false, .origin_uri = &self.origin_url.uri, }); @@ -442,12 +442,12 @@ pub const XMLHttpRequest = struct { // var used_body: ?XMLHttpRequestBodyInit = null; if (body) |b| { if (self.method != .GET and self.method != .HEAD) { - request.body = try session_state.arena.dupe(u8, b); + request.body = try page.arena.dupe(u8, b); try request.addHeader("Content-Type", "text/plain; charset=UTF-8", .{}); } } - try request.sendAsync(session_state.loop, self, .{}); + try request.sendAsync(page.loop, self, .{}); } pub fn onHttpResponse(self: *XMLHttpRequest, progress_: anyerror!http.Progress) !void { diff --git a/src/browser/xmlserializer/xmlserializer.zig b/src/browser/xmlserializer/xmlserializer.zig index 207db972..17c362f8 100644 --- a/src/browser/xmlserializer/xmlserializer.zig +++ b/src/browser/xmlserializer/xmlserializer.zig @@ -18,7 +18,7 @@ // const std = @import("std"); -const SessionState = @import("../env.zig").SessionState; +const Page = @import("../page.zig").Page; const dump = @import("../dump.zig"); const parser = @import("../netsurf.zig"); @@ -33,8 +33,8 @@ pub const XMLSerializer = struct { return .{}; } - pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 { - var buf = std.ArrayList(u8).init(state.arena); + pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, page: *Page) ![]const u8 { + var buf = std.ArrayList(u8).init(page.arena); if (try parser.nodeType(root) == .document) { try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()); } else { diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 53644410..19ba3c71 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -555,7 +555,7 @@ const IsolatedWorld = struct { // 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.scope != null) return error.Only1IsolatedContextSupported; - _ = try self.executor.startScope(&page.window, &page.state, {}, false); + _ = try self.executor.startScope(&page.window, page, {}, false); } }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 53d8e19a..9af5c89e 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -383,6 +383,7 @@ fn getContentQuads(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const page = bc.session.currentPage() orelse return error.PageNotLoaded; const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); @@ -397,7 +398,7 @@ fn getContentQuads(cmd: anytype) !void { // Elements like SVGElement may have multiple quads. const element = parser.nodeToElement(node._node); - const rect = try Element._getBoundingClientRect(element, &bc.session.page.?.state); + const rect = try Element._getBoundingClientRect(element, page); const quad = rectToQuad(rect); return cmd.sendResult(.{ .quads = &.{quad} }, .{}); @@ -411,6 +412,7 @@ fn getBoxModel(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const page = bc.session.currentPage() orelse return error.PageNotLoaded; const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); @@ -418,7 +420,7 @@ fn getBoxModel(cmd: anytype) !void { if (try parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; const element = parser.nodeToElement(node._node); - const rect = try Element._getBoundingClientRect(element, &bc.session.page.?.state); + const rect = try Element._getBoundingClientRect(element, page); const quad = rectToQuad(rect); return cmd.sendResult(.{ .model = BoxModel{ diff --git a/src/main_wpt.zig b/src/main_wpt.zig index cd2b5536..614b4968 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -113,10 +113,10 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *? }); defer runner.deinit(); - try polyfill.load(arena, runner.scope); + try polyfill.load(arena, runner.page.scope); // loop over the scripts. - const doc = parser.documentHTMLToDocument(runner.state.window.document); + const doc = parser.documentHTMLToDocument(runner.page.window.document); const scripts = try parser.documentGetElementsByTagName(doc, "script"); const script_count = try parser.nodeListLength(scripts); for (0..script_count) |i| { @@ -147,7 +147,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *? try parser.eventInit(loadevt, "load", .{}); _ = try parser.eventTargetDispatchEvent( - parser.toEventTarget(@TypeOf(runner.window), &runner.window), + parser.toEventTarget(@TypeOf(runner.page.window), &runner.page.window), loadevt, ); } @@ -155,9 +155,9 @@ 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.scope); + try_catch.init(runner.page.scope); defer try_catch.deinit(); - try runner.loop.run(); + try runner.page.loop.run(); if (try_catch.hasCaught()) { err_out.* = (try try_catch.err(arena)) orelse "unknwon error"; diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig index c51298c0..8cfc5c44 100644 --- a/src/runtime/testing.zig +++ b/src/runtime/testing.zig @@ -23,7 +23,7 @@ const generate = @import("generate.zig"); pub const allocator = std.testing.allocator; // Very similar to the JSRunner in src/testing.zig, but it isn't tied to the -// browser.Env or the browser.SessionState +// browser.Env or the *Page state pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type { const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types; diff --git a/src/telemetry/telemetry.zig b/src/telemetry/telemetry.zig index b3bc6b06..93b0e4e1 100644 --- a/src/telemetry/telemetry.zig +++ b/src/telemetry/telemetry.zig @@ -183,7 +183,7 @@ test "telemetry: getOrCreateId" { } test "telemetry: sends event to provider" { - var app = testing.app(.{}); + var app = testing.createApp(.{}); defer app.deinit(); var telemetry = TelemetryT(MockProvider).init(app, .serve); diff --git a/src/testing.zig b/src/testing.zig index d800afb5..faf3455d 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -171,7 +171,7 @@ pub fn print(comptime fmt: []const u8, args: anytype) void { } // dummy opts incase we want to add something, and not have to break all the callers -pub fn app(_: anytype) *App { +pub fn createApp(_: anytype) *App { return App.init(allocator, .{ .run_mode = .serve }) catch unreachable; } @@ -367,114 +367,79 @@ pub const tracking_allocator = @import("root").tracking_allocator.allocator(); pub const JsRunner = struct { const URL = @import("url.zig").URL; const Env = @import("browser/env.zig").Env; - const Loop = @import("runtime/loop.zig").Loop; - const HttpClient = @import("http/client.zig").Client; - const storage = @import("browser/storage/storage.zig"); - const Window = @import("browser/html/window.zig").Window; - const Renderer = @import("browser/renderer.zig").Renderer; - const SessionState = @import("browser/env.zig").SessionState; + const Page = @import("browser/page.zig").Page; + const Browser = @import("browser/browser.zig").Browser; - url: URL, - env: *Env, - loop: Loop, - window: Window, - state: SessionState, - arena: Allocator, - renderer: Renderer, - http_client: HttpClient, - scope: *Env.Scope, - executor: Env.ExecutionWorld, - storage_shelf: storage.Shelf, - cookie_jar: storage.CookieJar, + app: *App, + page: *Page, + browser: *Browser, - fn init(parent_allocator: Allocator, opts: RunnerOpts) !*JsRunner { + fn init(alloc: Allocator, opts: RunnerOpts) !JsRunner { parser.deinit(); - try parser.init(); - const aa = try parent_allocator.create(std.heap.ArenaAllocator); - aa.* = std.heap.ArenaAllocator.init(parent_allocator); - errdefer aa.deinit(); - - const arena = aa.allocator(); - const self = try arena.create(JsRunner); - self.arena = arena; - - self.env = try Env.init(arena, .{}); - errdefer self.env.deinit(); - - self.url = try URL.parse(opts.url, null); - - self.renderer = Renderer.init(arena); - self.cookie_jar = storage.CookieJar.init(arena); - self.loop = try Loop.init(arena); - errdefer self.loop.deinit(); - - var html = std.io.fixedBufferStream(opts.html); - const document = try parser.documentHTMLParse(html.reader(), "UTF-8"); - - self.window = try Window.create(null, null); - try self.window.replaceDocument(document); - try self.window.replaceLocation(.{ - .url = try self.url.toWebApi(arena), - }); - - self.http_client = try HttpClient.init(arena, 1, .{ + var app = try App.init(alloc, .{ + .run_mode = .serve, .tls_verify_host = false, }); + errdefer app.deinit(); - self.state = .{ - .arena = arena, - .loop = &self.loop, - .url = &self.url, - .window = &self.window, - .renderer = &self.renderer, - .cookie_jar = &self.cookie_jar, - .request_factory = self.http_client.requestFactory(null), + const browser = try alloc.create(Browser); + errdefer alloc.destroy(browser); + + browser.* = try Browser.init(app); + errdefer browser.deinit(); + + var session = try browser.newSession(); + + var page = try session.createPage(); + + // a bit hacky, but since we aren't going through page.navigate, there's + // some minimum setup we need to do + page.url = try URL.parse(opts.url, null); + try page.window.replaceLocation(.{ + .url = try page.url.toWebApi(page.arena), + }); + + var html = std.io.fixedBufferStream(opts.html); + try page.loadHTMLDoc(html.reader(), "UTF-8"); + + return .{ + .app = app, + .page = page, + .browser = browser, }; - - self.storage_shelf = storage.Shelf.init(arena); - self.window.setStorageShelf(&self.storage_shelf); - - self.executor = try self.env.newExecutionWorld(); - errdefer self.executor.deinit(); - - self.scope = try self.executor.startScope(&self.window, &self.state, {}, true); - return self; } pub fn deinit(self: *JsRunner) void { - self.loop.deinit(); - self.executor.deinit(); - self.env.deinit(); - self.http_client.deinit(); - self.storage_shelf.deinit(); - - const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr)); - arena.deinit(); - arena.child_allocator.destroy(arena); + self.browser.deinit(); + self.app.allocator.destroy(self.browser); + self.app.deinit(); } const RunOpts = struct {}; pub const Case = std.meta.Tuple(&.{ []const u8, ?[]const u8 }); pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void { + 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(self.scope); + try_catch.init(scope); defer try_catch.deinit(); - const value = self.scope.exec(case.@"0", null) catch |err| { - if (try try_catch.err(self.arena)) |msg| { + 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" }); } return err; }; - try self.loop.run(); + try self.page.loop.run(); @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); if (case.@"1") |expected| { - const actual = try value.toString(self.arena); + const actual = try value.toString(arena); if (std.mem.eql(u8, expected, actual) == false) { std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ expected, actual, i + 1, case.@"0" }); return error.UnexpectedResult; @@ -488,12 +453,15 @@ pub const JsRunner = struct { } pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value { + const scope = self.page.scope; + const arena = self.page.arena; + var try_catch: Env.TryCatch = undefined; - try_catch.init(self.scope); + try_catch.init(scope); defer try_catch.deinit(); - return self.scope.exec(src, name) catch |err| { - if (try try_catch.err(self.arena)) |msg| { + 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}); } @@ -517,6 +485,6 @@ const RunnerOpts = struct { , }; -pub fn jsRunner(alloc: Allocator, opts: RunnerOpts) !*JsRunner { +pub fn jsRunner(alloc: Allocator, opts: RunnerOpts) !JsRunner { return JsRunner.init(alloc, opts); } From 676e6ecec1db7ce3b867d5d6e514234895454259 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 27 May 2025 20:18:14 +0800 Subject: [PATCH 2/2] fix/revert debug code --- src/browser/polyfill/fetch.zig | 24 ++++++++++++------------ src/browser/storage/cookie.zig | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/browser/polyfill/fetch.zig b/src/browser/polyfill/fetch.zig index 76d7cc58..d062a2cb 100644 --- a/src/browser/polyfill/fetch.zig +++ b/src/browser/polyfill/fetch.zig @@ -31,16 +31,16 @@ test "Browser.fetch" { .{ "ok", "true" }, }, .{}); - // try runner.testCases(&.{ - // .{ - // \\ var ok2 = false; - // \\ const request2 = new Request("http://127.0.0.1:9582/loader"); - // \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); - // \\ false; - // , - // "false", - // }, - // // all events have been resolved. - // .{ "ok2", "true" }, - // }, .{}); + try runner.testCases(&.{ + .{ + \\ var ok2 = false; + \\ const request2 = new Request("http://127.0.0.1:9582/loader"); + \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); + \\ false; + , + "false", + }, + // all events have been resolved. + .{ "ok2", "true" }, + }, .{}); } diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index 89e41ed4..a25935ad 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -231,7 +231,7 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool { fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool { const origin_uri = origin_uri_ orelse return true; - const origin_host = (origin_uri.host orelse return error.InvalidURI333).percent_encoded; + const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; // common case if (std.mem.eql(u8, target_host, origin_host)) {