diff --git a/build.zig b/build.zig index e7527cff..28e76aa1 100644 --- a/build.zig +++ b/build.zig @@ -104,10 +104,10 @@ pub fn build(b: *Build) !void { }); check.dependOn(&check_lib.step); - // Extras (snapshot_creator, legacy_test) are off the default install to + // Extras (snapshot_creator) are off the default install to // avoid paying for three exe compiles on every edit. Build explicitly // with `zig build extras`. - const extras_step = b.step("extras", "Build snapshot_creator and legacy_test"); + const extras_step = b.step("extras", "Build snapshot_creator"); { // browser @@ -187,38 +187,6 @@ pub fn build(b: *Build) !void { const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_tests.step); } - - { - // browser - const exe = b.addExecutable(.{ - .name = "legacy_test", - .use_llvm = true, - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main_legacy_test.zig"), - .target = target, - .optimize = optimize, - .sanitize_c = enable_csan, - .sanitize_thread = enable_tsan, - .imports = &.{ - .{ .name = "lightpanda", .module = lightpanda_module }, - }, - }), - }); - extras_step.dependOn(&b.addInstallArtifact(exe, .{}).step); - - const exe_check = b.addLibrary(.{ - .name = "legacy_test_check", - .root_module = exe.root_module, - }); - check.dependOn(&exe_check.step); - - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } - const run_step = b.step("legacy_test", "Run the app"); - run_step.dependOn(&run_cmd.step); - } } fn linkV8( diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index a6d157c2..2fed8d63 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -90,8 +90,13 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi }; self.handler(&req) catch |err| { - std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err }); - try req.respond("server error", .{ .status = .internal_server_error }); + switch (err) { + error.BrokenPipe => {}, + else => { + std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err }); + try req.respond("server error", .{ .status = .internal_server_error }); + }, + } return; }; } diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 9288cc4f..248ededa 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -282,7 +282,13 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, frame: ._type = unionInit(AbstractRange.Type, chain.get(1)), }; chain.setLeaf(1, child); - frame._live_ranges.append(&abstract_range._range_link); + + if (abstract_range._type != .static_range) { + // StaticRanges are not live, so they don't get added to the frame's + // live_ranges list. + frame._live_ranges.append(&abstract_range._range_link); + } + return chain.get(1); } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 024093ca..50251fa8 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -307,6 +307,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { ._event_manager = EventManager.init(arena, self), }; self._to_load = &self._to_load_1; + self._http_owner.blob_urls = &self._blob_urls; var screen: *Screen = undefined; var visual_viewport: *VisualViewport = undefined; @@ -520,11 +521,6 @@ pub fn isSameOrigin(self: *const Frame, url: [:0]const u8) bool { return std.mem.eql(u8, URL.getHost(url), URL.getHost(current_origin)); } -/// Look up a blob URL in this frame's registry. -pub fn lookupBlobUrl(self: *Frame, url: []const u8) ?*Blob { - return self._blob_urls.get(url); -} - pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !void { lp.assert(self._load_state == .waiting, "frame.renavigate", .{}); const session = self._session; @@ -585,7 +581,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo }; const parse_arena = try self.getArena(.medium, "Frame.parseBlob"); defer self.releaseArena(parse_arena); - var parser = Parser.init(parse_arena, self.document.asNode(), self); + var parser = Parser.init(parse_arena, self.document.asNode(), self, .{ .allow_declarative_shadow = true }); parser.parse(blob._slice); } else { self.document.injectBlank(self) catch |err| { @@ -1196,7 +1192,7 @@ fn frameDoneCallback(ctx: *anyopaque) !void { const parse_arena = try self.getArena(.medium, "Frame.parse"); defer self.releaseArena(parse_arena); - var parser = Parser.init(parse_arena, self.document.asNode(), self); + var parser = Parser.init(parse_arena, self.document.asNode(), self, .{ .allow_declarative_shadow = true }); switch (self._parse_state) { .html => |*html| { @@ -3264,7 +3260,9 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No if (should_notify) { if (comptime from_parser == false) { // When the parser adds the node, nodeIsReady is only called when the - // nodeComplete() callback is executed. + // nodeComplete() callback is executed. nodeIsReady resolves the + // node's owning frame itself (only for the few node types that have + // ready work), so pass the incumbent `self`. try self.nodeIsReady(false, child); // Check if text was added to a script that hasn't started yet. @@ -3573,11 +3571,20 @@ pub fn updateRangesForNodeRemoval(self: *Frame, parent: *Node, child: *Node, chi // TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '') pub fn parseHtmlAsChildren(self: *Frame, node: *Node, html: []const u8) !void { + return self.parseHtmlAsChildrenInner(node, html, false); +} + +// setHTMLUnsafe variant: parse a fragment that may contain declarative shadow node +pub fn parseHtmlUnsafeAsChildren(self: *Frame, node: *Node, html: []const u8) !void { + return self.parseHtmlAsChildrenInner(node, html, true); +} + +fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, allow_declarative_shadow: bool) !void { const previous_parse_mode = self._parse_mode; self._parse_mode = .fragment; defer self._parse_mode = previous_parse_mode; - var parser = Parser.init(self.call_arena, node, self); + var parser = Parser.init(self.call_arena, node, self, .{ .allow_declarative_shadow = allow_declarative_shadow }); parser.parseFragment(html); // html5ever wraps fragment output in an element; unwrap so its @@ -3615,6 +3622,15 @@ fn nodeIsReady(self: *Frame, comptime from_parser: bool, node: *Node) !void { // we don't execute scripts added via innerHTML = ', loading an - - - - diff --git a/src/browser/tests/legacy/window/window.html b/src/browser/tests/legacy/window/window.html deleted file mode 100644 index dee57062..00000000 --- a/src/browser/tests/legacy/window/window.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/tests/legacy/xhr/form_data.html b/src/browser/tests/legacy/xhr/form_data.html deleted file mode 100644 index cda34c06..00000000 --- a/src/browser/tests/legacy/xhr/form_data.html +++ /dev/null @@ -1,133 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - diff --git a/src/browser/tests/legacy/xhr/progress_event.html b/src/browser/tests/legacy/xhr/progress_event.html deleted file mode 100644 index 4b7f5df4..00000000 --- a/src/browser/tests/legacy/xhr/progress_event.html +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/browser/tests/legacy/xhr/xhr.html b/src/browser/tests/legacy/xhr/xhr.html deleted file mode 100644 index c7c557bd..00000000 --- a/src/browser/tests/legacy/xhr/xhr.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - - - - - -

And

- diff --git a/src/browser/tests/navigator/navigator.html b/src/browser/tests/navigator/navigator.html index 92f33bab..09419d69 100644 --- a/src/browser/tests/navigator/navigator.html +++ b/src/browser/tests/navigator/navigator.html @@ -27,6 +27,36 @@ testing.expectEqual('Gecko', navigator.product); testing.expectEqual(false, navigator.javaEnabled()); testing.expectEqual(false, navigator.webdriver); + + testing.expectEqual(null, navigator.doNotTrack); + + // Every Navigator attribute must be a native accessor on the prototype + for (const name of [ + 'userAgent', 'appName', 'appCodeName', 'appVersion', 'platform', + 'language', 'languages', 'onLine', 'cookieEnabled', 'hardwareConcurrency', + 'deviceMemory', 'maxTouchPoints', 'vendor', 'product', 'webdriver', + 'plugins', 'doNotTrack', 'globalPrivacyControl', + ]) { + const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, name); + testing.expectEqual('function', typeof desc.get); // accessor on prototype + testing.expectEqual(undefined, desc.value); // not a data property + testing.expectEqual(undefined, Object.getOwnPropertyDescriptor(navigator, name)); // not own + testing.expectEqual(true, desc.get.toString().includes('[native code]')); + } + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/staticrange.html b/src/browser/tests/staticrange.html new file mode 100644 index 00000000..c11952ef --- /dev/null +++ b/src/browser/tests/staticrange.html @@ -0,0 +1,136 @@ + + + +
abcdefghi
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/window/replaceable.html b/src/browser/tests/window/replaceable.html new file mode 100644 index 00000000..21828378 --- /dev/null +++ b/src/browser/tests/window/replaceable.html @@ -0,0 +1,33 @@ + + + + + + diff --git a/src/browser/tests/worker/replaceable-worker.js b/src/browser/tests/worker/replaceable-worker.js new file mode 100644 index 00000000..525ded43 --- /dev/null +++ b/src/browser/tests/worker/replaceable-worker.js @@ -0,0 +1,27 @@ +// `console` and `self` are [Replaceable] on WorkerGlobalScope: assignment must +// replace the value even in strict mode, rather than throwing through the +// getter-only accessor. Capture the global in a local binding first, since +// `self` is itself replaceable (reassigning it would break global lookups). +(function () { + "use strict"; + const g = self; + try { + const results = {}; + + const prevConsole = g.console; + g.console = { marker: 7 }; + results.console_replaced = g.console.marker === 7; + g.console = prevConsole; + results.console_restored = g.console === prevConsole; + + const prevSelf = g.self; + g.self = "REPLACED"; + results.self_replaced = g.self === "REPLACED"; + g.self = prevSelf; + results.self_restored = g.self === prevSelf; + + g.postMessage({ ok: true, results }); + } catch (e) { + g.postMessage({ ok: false, err: String(e) }); + } +})(); diff --git a/src/browser/tests/worker/worker.html b/src/browser/tests/worker/worker.html index 8b15346a..cabe5d14 100644 --- a/src/browser/tests/worker/worker.html +++ b/src/browser/tests/worker/worker.html @@ -172,6 +172,25 @@ } + + + + diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index f6bd450f..88bc4ee4 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -130,7 +130,7 @@ fn markAborted(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !vo fn dispatchAbortEvent(self: *AbortSignal, exec: *const Execution) !void { const target = self.asEventTarget(); const on_abort = self._on_abort; - switch (exec.context.global) { + switch (exec.js.global) { inline else => |g| { if (g._event_manager.hasDirectListeners(target, "abort", on_abort)) { const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._page); @@ -191,7 +191,7 @@ const ThrowIfAborted = union(enum) { undefined: void, }; pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIfAborted { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self._aborted) { const exception = switch (self._reason) { diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index 2a530d9b..732acc7e 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -24,6 +24,7 @@ const Page = @import("../Page.zig"); const Node = @import("Node.zig"); const Range = @import("Range.zig"); +const StaticRange = @import("StaticRange.zig"); const Allocator = std.mem.Allocator; @@ -48,8 +49,11 @@ pub fn acquireRef(self: *AbstractRange) void { } pub fn deinit(self: *AbstractRange, page: *Page) void { - if (page.findFrameByLoaderId(self._frame_loader_id)) |frame| { - frame._live_ranges.remove(&self._range_link); + // StaticRanges are never registered in the live-range list + if (self._type != .static_range) { + if (page.findFrameByLoaderId(self._frame_loader_id)) |frame| { + frame._live_ranges.remove(&self._range_link); + } } page.releaseArena(self._arena); } @@ -60,7 +64,7 @@ pub fn releaseRef(self: *AbstractRange, page: *Page) void { pub const Type = union(enum) { range: *Range, - // TODO: static_range: *StaticRange, + static_range: *StaticRange, }; pub fn as(self: *AbstractRange, comptime T: type) *T { @@ -70,6 +74,7 @@ pub fn as(self: *AbstractRange, comptime T: type) *T { pub fn is(self: *AbstractRange, comptime T: type) ?*T { switch (self._type) { .range => |r| return if (T == Range) r else null, + .static_range => |sr| return if (T == StaticRange) sr else null, } } @@ -339,5 +344,4 @@ pub const JsApi = struct { pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{}); pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{}); pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{}); - pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{}); }; diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 4b393b50..df09e16d 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -261,7 +261,7 @@ pub fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: /// Returns a Promise that resolves with the contents of the blob /// as binary data contained in an ArrayBuffer. pub fn arrayBuffer(self: *const Blob, exec: *Execution) !js.Promise { - return exec.context.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice }); + return exec.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice }); } const ReadableStream = @import("streams/ReadableStream.zig"); @@ -274,7 +274,7 @@ pub fn stream(self: *const Blob, exec: *Execution) !*ReadableStream { /// Returns a Promise that resolves with a string containing /// the contents of the blob, interpreted as UTF-8. pub fn text(self: *const Blob, exec: *Execution) !js.Promise { - return exec.context.local.?.resolvePromise(self._slice); + return exec.js.local.?.resolvePromise(self._slice); } /// Extension to Blob; works on Firefox and Safari. @@ -282,7 +282,7 @@ pub fn text(self: *const Blob, exec: *Execution) !js.Promise { /// Returns a Promise that resolves with a Uint8Array containing /// the contents of the blob as an array of bytes. pub fn bytes(self: *const Blob, exec: *Execution) !js.Promise { - return exec.context.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice }); + return exec.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice }); } /// Returns a new Blob object which contains data diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index b38a324e..23be6a1e 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -34,7 +34,7 @@ _counts: std.StringHashMapUnmanaged(u64) = .{}, pub const init: Console = .{}; fn dispatchConsoleMessage(values: []js.Value, console_type: Notification.ConsoleMessageType, exec: *js.Execution) void { - const notification = exec.context.page.session.notification; + const notification = exec.session.notification; const ts = datetime.timestamp(.monotonic); notification.dispatch(.console_message, &.{ @@ -54,7 +54,7 @@ fn dispatchConsoleMessage(values: []js.Value, console_type: Notification.Console pub fn trace(_: *const Console, values: []js.Value, exec: *js.Execution) !void { logger.debug(.js, "console.trace", .{ - .stack = exec.context.local.?.stackTrace() catch "???", + .stack = exec.js.local.?.stackTrace() catch "???", .args = ValueWriter{ .values = values }, }); dispatchConsoleMessage(values, .trace, exec); @@ -91,7 +91,7 @@ pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, exec: } pub fn @"error"(_: *const Console, values: []js.Value, exec: *js.Execution) void { - logger.warn(.js, "console.error", .{ValueWriter{ .values = values, .stack = exec.context.local.?.stackTrace() catch |err| @errorName(err) orelse "???" }}); + logger.warn(.js, "console.error", .{ValueWriter{ .values = values, .stack = exec.js.local.?.stackTrace() catch |err| @errorName(err) orelse "???" }}); dispatchConsoleMessage(values, .@"error", exec); } diff --git a/src/browser/webapi/CustomElementDefinition.zig b/src/browser/webapi/CustomElementDefinition.zig index 2e60a8ac..6b3eab48 100644 --- a/src/browser/webapi/CustomElementDefinition.zig +++ b/src/browser/webapi/CustomElementDefinition.zig @@ -36,6 +36,9 @@ observed_attributes: std.StringHashMapUnmanaged(void) = .{}, // For autonomous custom elements, this is null extends: ?Element.Tag = null, +// when disabledFeatures = ["shadow"], we'll throw if attachShadow is called +disable_shadow: bool = false, + pub fn isAttributeObserved(self: *const CustomElementDefinition, name: String) bool { return self.observed_attributes.contains(name.str()); } diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 4b2e0fe3..a1dbbf5e 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -81,6 +81,20 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu } } + // Read disabledFeatures static property: ["shadow"] makes attachShadow throw. + if (constructor.getPropertyValue("disabledFeatures") catch null) |disabled| { + if (disabled.isArray()) { + var js_arr = disabled.toArray(); + for (0..js_arr.len()) |i| { + const val = js_arr.get(@intCast(i)) catch continue; + const feature = val.toSSO(false) catch continue; + if (feature.eql(comptime .wrap("shadow"))) { + definition.disable_shadow = true; + } + } + } + } + gop.key_ptr.* = owned_name; gop.value_ptr.* = definition; diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index 8ab2fb9d..c85b497d 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -77,7 +77,7 @@ pub fn parseFromString( } // Parse HTML into the document - var parser = Parser.init(arena, doc.asNode(), frame); + var parser = Parser.init(arena, doc.asNode(), frame, .{}); parser.parse(normalized); if (parser.err) |pe| { @@ -94,13 +94,13 @@ pub fn parseFromString( // Parse XML into XMLDocument. const doc_node = doc.asNode(); - var parser = Parser.init(arena, doc_node, frame); + var parser = Parser.init(arena, doc_node, frame, .{}); parser.parseXML(html); if (parser.err != null or doc_node.firstChild() == null) { // Return a document with a element per spec. const err_doc = try frame._factory.document(XMLDocument{ ._proto = undefined }); - var err_parser = Parser.init(arena, err_doc.asNode(), frame); + var err_parser = Parser.init(arena, err_doc.asNode(), frame, .{}); err_parser.parseXML("error"); return err_doc.asDocument(); } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 6cd67392..44f777a4 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -786,7 +786,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool const arena = try frame.getArena(.medium, "Document.write"); defer frame.releaseArena(arena); - var parser = Parser.init(arena, fragment_node, frame); + var parser = Parser.init(arena, fragment_node, frame, .{ .allow_declarative_shadow = true }); parser.parseFragment(html); // Extract children from wrapper HTML element (html5ever wraps fragments) @@ -873,7 +873,7 @@ pub fn open(self: *Document, call_frame: *Frame) !*Document { self._implementation = null; self._ready_state = .loading; - self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame); + self._script_created_parser = Parser.Streaming.init(frame.arena, doc_node, frame, .{ .allow_declarative_shadow = true }); try self._script_created_parser.?.start(); frame._parse_mode = .document; diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index cc692649..324af261 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -153,18 +153,13 @@ pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Fra pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void { const parent = self.asNode(); + return parent.setHTML(html, false, frame); +} - frame.domChanged(); - var it = parent.childrenIterator(); - while (it.next()) |child| { - frame.removeNode(parent, child, .{ .will_be_reconnected = false }); - } - - if (html.len == 0) { - return; - } - - try frame.parseHtmlAsChildren(parent, html); +/// allows declarative shadow dom +pub fn setHTMLUnsafe(self: *DocumentFragment, html: []const u8, frame: *Frame) !void { + const parent = self.asNode(); + return parent.setHTML(html, true, frame); } pub fn cloneFragment(self: *DocumentFragment, deep: bool, frame: *Frame) !*Node { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 98eef93a..3a26798e 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -484,18 +484,13 @@ pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void { const parent = self.asNode(); + return parent.setHTML(html, false, frame); +} - frame.domChanged(); - var it = parent.childrenIterator(); - while (it.next()) |child| { - frame.removeNode(parent, child, .{ .will_be_reconnected = false }); - } - - if (html.len == 0) { - return; - } - - try frame.parseHtmlAsChildren(parent, html); +/// allows declarative shadow dom +pub fn setHTMLUnsafe(self: *Element, html: []const u8, frame: *Frame) !void { + const parent = self.asNode(); + return parent.setHTML(html, true, frame); } pub fn getId(self: *const Element) []const u8 { @@ -726,11 +721,41 @@ pub fn getAssignedSlot(self: *Element, frame: *Frame) ?*Html.Slot { return frame._element_assigned_slots.get(self); } -pub fn attachShadow(self: *Element, mode_str: []const u8, frame: *Frame) !*ShadowRoot { - if (frame._element_shadow_roots.get(self)) |_| { - return error.AlreadyHasShadowRoot; +// Whether this element may host a shadow root +fn isValidShadowHost(self: *const Element) bool { + if (self._namespace != .html) { + return false; } - const mode = try ShadowRoot.Mode.fromString(mode_str); + + return switch (self.getTag()) { + .article, .aside, .blockquote, .body, .div, .footer, .header, .main, .nav, .p, .section, .span, .h1, .h2, .h3, .h4, .h5, .h6, .custom => true, + else => false, + }; +} + +pub fn attachShadow(self: *Element, mode_str: String, frame: *Frame) !*ShadowRoot { + if (frame._element_shadow_roots.get(self)) |_| { + return error.NotSupported; + } + if (!self.isValidShadowHost()) { + return error.NotSupported; + } + + // A custom element whose definition lists "shadow" in disabledFeatures + // cannot host a shadow root (imperative or declarative). + if (self.is(Html.Custom)) |custom| { + if (frame.window._custom_elements._definitions.get(custom._tag_name.str())) |def| { + if (def.disable_shadow) { + return error.NotSupported; + } + } + } + const mode: ShadowRoot.Mode = blk: { + if (mode_str.eql(comptime .wrap("open"))) break :blk .open; + if (mode_str.eql(comptime .wrap("closed"))) break :blk .closed; + return error.InvalidArgument; + }; + const shadow_root = try ShadowRoot.init(self, mode, frame); try frame._element_shadow_roots.put(frame.arena, self, shadow_root); return shadow_root; @@ -1792,11 +1817,12 @@ pub const JsApi = struct { pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{}); pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true }); pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true }); + pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true }); pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true }); pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true }); const ShadowRootInit = struct { - mode: []const u8, + mode: String, }; fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot { return self.attachShadow(init.mode, frame); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 5e66e42d..a59c2210 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -329,7 +329,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget { // Add window at the end (unless we stopped at shadow boundary) if (!stopped_at_shadow_boundary) { if (path_len < path_buffer.len) { - switch (exec.context.global) { + switch (exec.js.global) { .worker => {}, .frame => |frame| { path_buffer[path_len] = frame.window.asEventTarget(); diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 4c18cd31..01505d5e 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -63,7 +63,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo } event._is_trusted = false; - switch (exec.context.global) { + switch (exec.js.global) { .frame => |frame| { event.acquireRef(); defer _ = event.releaseRef(frame._page); @@ -99,7 +99,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi }; }; - switch (exec.context.global) { + switch (exec.js.global) { inline else => |g| _ = try g._event_manager.register(self, typ, em_callback, options), } } @@ -135,7 +135,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even }; }; - switch (exec.context.global) { + switch (exec.js.global) { inline else => |g| g._event_manager.remove(self, typ, em_callback, use_capture), } } diff --git a/src/browser/webapi/FileReader.zig b/src/browser/webapi/FileReader.zig index 3c352443..9924f4ac 100644 --- a/src/browser/webapi/FileReader.zig +++ b/src/browser/webapi/FileReader.zig @@ -257,7 +257,7 @@ fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Pr const event = (try ProgressEvent.initTrusted( comptime .wrap(typ), .{ .total = progress.total, .loaded = progress.loaded }, - exec.context.page, + exec.page, )).asEvent(); return exec.dispatch( diff --git a/src/browser/webapi/ImageData.zig b/src/browser/webapi/ImageData.zig index b295a7e7..333630e8 100644 --- a/src/browser/webapi/ImageData.zig +++ b/src/browser/webapi/ImageData.zig @@ -80,7 +80,7 @@ pub fn init( return exec._factory.create(ImageData{ ._width = width, ._height = height, - ._data = try exec.context.local.?.createTypedArray(.uint8_clamped, size).persist(), + ._data = try exec.js.local.?.createTypedArray(.uint8_clamped, size).persist(), }); } diff --git a/src/browser/webapi/ModelContext.zig b/src/browser/webapi/ModelContext.zig index 19323a3f..ae79fb4b 100644 --- a/src/browser/webapi/ModelContext.zig +++ b/src/browser/webapi/ModelContext.zig @@ -121,7 +121,7 @@ pub fn registerTool( // native MCP forwarder) can surface the new tool. const event: Notification.ModelContextToolEvent = .{ .exec = exec, .tool = entry }; - const session = switch (exec.context.global) { + const session = switch (exec.js.global) { inline else => |g| g._session, }; @@ -147,7 +147,7 @@ pub fn findTool(self: *ModelContext, name: []const u8) ?*Tool { /// dispatching `model_context_tool_removed` for each. Cheap when no /// signals fired (which is the common case). fn markAborted(self: *ModelContext, tool: *Tool, exec: *const Execution) !void { - const session = switch (exec.context.global) { + const session = switch (exec.js.global) { inline else => |g| g._session, }; @@ -191,7 +191,7 @@ pub const ModelContextClient = struct { exec: *const Execution, ) !js.Promise { var ls: js.Local.Scope = undefined; - exec.context.global.getJs().localScope(&ls); + exec.js.global.getJs().localScope(&ls); defer ls.deinit(); const resolver = ls.local.createPromiseResolver(); diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index c51e3da2..958fd109 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -48,6 +48,62 @@ pub fn getLanguages(_: *const Navigator) [2][]const u8 { return .{ "en-US", "en" }; } +pub fn getDoNotTrack(_: *const Navigator) ?[]const u8 { + return null; +} + +pub fn getAppName(_: *const Navigator) []const u8 { + return "Netscape"; +} + +pub fn getAppCodeName(_: *const Navigator) []const u8 { + return "Mozilla"; +} + +pub fn getAppVersion(_: *const Navigator) []const u8 { + return "1.0"; +} + +pub fn getLanguage(_: *const Navigator) []const u8 { + return "en-US"; +} + +pub fn getOnLine(_: *const Navigator) bool { + return true; +} + +pub fn getCookieEnabled(_: *const Navigator) bool { + return true; +} + +pub fn getHardwareConcurrency(_: *const Navigator) u32 { + return 4; +} + +pub fn getDeviceMemory(_: *const Navigator) f64 { + return 8.0; +} + +pub fn getMaxTouchPoints(_: *const Navigator) u32 { + return 0; +} + +pub fn getVendor(_: *const Navigator) []const u8 { + return ""; +} + +pub fn getProduct(_: *const Navigator) []const u8 { + return "Gecko"; +} + +pub fn getWebdriver(_: *const Navigator) bool { + return false; +} + +pub fn getGlobalPrivacyControl(_: *const Navigator) bool { + return true; +} + pub fn getPlatform(_: *const Navigator) []const u8 { return switch (builtin.os.tag) { .macos => "MacIntel", @@ -166,25 +222,27 @@ pub const JsApi = struct { pub const empty_with_no_proto = true; }; - // Read-only properties + // Read-only properties. All are accessors (not data properties) so they + // present as native getters on Navigator.prototype, matching real browsers + // — see the getter definitions above for why. pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{}); - pub const appName = bridge.property("Netscape", .{ .template = false }); - pub const appCodeName = bridge.property("Mozilla", .{ .template = false }); - pub const appVersion = bridge.property("1.0", .{ .template = false }); + pub const appName = bridge.accessor(Navigator.getAppName, null, .{}); + pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{}); + pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{}); pub const platform = bridge.accessor(Navigator.getPlatform, null, .{}); - pub const language = bridge.property("en-US", .{ .template = false }); + pub const language = bridge.accessor(Navigator.getLanguage, null, .{}); pub const languages = bridge.accessor(Navigator.getLanguages, null, .{}); - pub const onLine = bridge.property(true, .{ .template = false }); - pub const cookieEnabled = bridge.property(true, .{ .template = false }); - pub const hardwareConcurrency = bridge.property(4, .{ .template = false }); - pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false }); - pub const maxTouchPoints = bridge.property(0, .{ .template = false }); - pub const vendor = bridge.property("", .{ .template = false }); - pub const product = bridge.property("Gecko", .{ .template = false }); - pub const webdriver = bridge.property(false, .{ .template = false }); + pub const onLine = bridge.accessor(Navigator.getOnLine, null, .{}); + pub const cookieEnabled = bridge.accessor(Navigator.getCookieEnabled, null, .{}); + pub const hardwareConcurrency = bridge.accessor(Navigator.getHardwareConcurrency, null, .{}); + pub const deviceMemory = bridge.accessor(Navigator.getDeviceMemory, null, .{}); + pub const maxTouchPoints = bridge.accessor(Navigator.getMaxTouchPoints, null, .{}); + pub const vendor = bridge.accessor(Navigator.getVendor, null, .{}); + pub const product = bridge.accessor(Navigator.getProduct, null, .{}); + pub const webdriver = bridge.accessor(Navigator.getWebdriver, null, .{}); pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{}); - pub const doNotTrack = bridge.property(null, .{ .template = false }); - pub const globalPrivacyControl = bridge.property(true, .{ .template = false }); + pub const doNotTrack = bridge.accessor(Navigator.getDoNotTrack, null, .{}); + pub const globalPrivacyControl = bridge.accessor(Navigator.getGlobalPrivacyControl, null, .{}); pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true }); pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true }); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 00779af0..c57913aa 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1076,6 +1076,25 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v } } +/// Shared implementation in Element and DocumentFragment +pub fn setHTML(self: *Node, html: []const u8, allow_declarative_shadow: bool, frame: *Frame) !void { + frame.domChanged(); + var it = self.childrenIterator(); + while (it.next()) |child| { + frame.removeNode(self, child, .{ .will_be_reconnected = false }); + } + + if (html.len == 0) { + return; + } + + if (allow_declarative_shadow) { + try frame.parseHtmlUnsafeAsChildren(self, html); + } else { + try frame.parseHtmlAsChildren(self, html); + } +} + // Writes a JSON representation of the node and its children pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void { // stupid json api requires this to be const, diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index d4a2de53..1c879e38 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -61,7 +61,7 @@ pub fn init(callback: js.Function.Global, exec: *const Execution) !*PerformanceO ._interests = 0, ._entries = .{}, ._performance = exec.performance(), - ._js = exec.context, + ._js = exec.js, ._arena = exec.arena, }); } diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 2a367093..e7c76873 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -43,6 +43,10 @@ pub fn asAbstractRange(self: *Range) *AbstractRange { return self._proto; } +pub fn getCommonAncestorContainer(self: *const Range) *Node { + return self._proto.getCommonAncestorContainer(); +} + pub fn setStart(self: *Range, node: *Node, offset: u32) !void { if (node._type == .document_type) { return error.InvalidNodeType; @@ -703,6 +707,7 @@ pub const JsApi = struct { pub const END_TO_START = bridge.property(3, .{ .template = true }); pub const constructor = bridge.constructor(Range.init, .{}); + pub const commonAncestorContainer = bridge.accessor(Range.getCommonAncestorContainer, null, .{}); pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true }); pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true }); pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true }); diff --git a/src/browser/webapi/ShadowRoot.zig b/src/browser/webapi/ShadowRoot.zig index 3af7ccfe..53e19289 100644 --- a/src/browser/webapi/ShadowRoot.zig +++ b/src/browser/webapi/ShadowRoot.zig @@ -29,10 +29,6 @@ const ShadowRoot = @This(); pub const Mode = enum { open, closed, - - pub fn fromString(str: []const u8) !Mode { - return std.meta.stringToEnum(Mode, str) orelse error.InvalidMode; - } }; _proto: *DocumentFragment, @@ -70,6 +66,10 @@ pub fn getHost(self: *const ShadowRoot) *Element { return self._host; } +pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void { + return self.asDocumentFragment().setHTMLUnsafe(html, frame); +} + pub fn getElementById(self: *ShadowRoot, id: []const u8, frame: *Frame) ?*Element { if (id.len == 0) { return null; @@ -137,6 +137,7 @@ pub const JsApi = struct { return self.getElementById(try value.toZig([]const u8), frame); } pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{}); + pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true }); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/StaticRange.zig b/src/browser/webapi/StaticRange.zig new file mode 100644 index 00000000..a9e5997b --- /dev/null +++ b/src/browser/webapi/StaticRange.zig @@ -0,0 +1,85 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const js = @import("../js/js.zig"); +const Frame = @import("../Frame.zig"); + +const Node = @import("Node.zig"); +const AbstractRange = @import("AbstractRange.zig"); + +const StaticRange = @This(); + +// The boundary points and `collapsed` accessor live on the shared AbstractRange +// prototype. Unlike Range, a StaticRange is *static*: the factory keeps it out +// of the frame's live-range list, so DOM mutations never move its boundaries. +_proto: *AbstractRange, + +// https://dom.spec.whatwg.org/#dictdef-staticrangeinit +// All members are required. The fields are non-optional with no default, so the +// argument decoder rejects a missing or null member with a TypeError. +pub const StaticRangeInit = struct { + startContainer: *Node, + startOffset: u32, + endContainer: *Node, + endOffset: u32, +}; + +pub fn init(opts: StaticRangeInit, frame: *Frame) !*StaticRange { + // https://dom.spec.whatwg.org/#dom-staticrange-staticrange + // Throw InvalidNodeTypeError if either container is a DocumentType or Attr. + // Note: offsets are stored verbatim — StaticRange does no length validation. + if (isInvalidContainer(opts.startContainer) or isInvalidContainer(opts.endContainer)) { + return error.InvalidNodeType; + } + + const arena = try frame.getArena(.medium, "StaticRange"); + errdefer frame.releaseArena(arena); + + const static_range = try frame._factory.abstractRange(arena, StaticRange{ ._proto = undefined }, frame); + const proto = static_range._proto; + proto._start_container = opts.startContainer; + proto._start_offset = opts.startOffset; + proto._end_container = opts.endContainer; + proto._end_offset = opts.endOffset; + return static_range; +} + +fn isInvalidContainer(node: *Node) bool { + return node._type == .document_type or node._type == .attribute; +} + +pub fn asAbstractRange(self: *StaticRange) *AbstractRange { + return self._proto; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(StaticRange); + + pub const Meta = struct { + pub const name = "StaticRange"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(StaticRange.init, .{ .dom_exception = true }); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: StaticRange" { + try testing.htmlRunner("staticrange.html", .{}); +} diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig index 3220352c..242938aa 100644 --- a/src/browser/webapi/SubtleCrypto.zig +++ b/src/browser/webapi/SubtleCrypto.zig @@ -51,7 +51,7 @@ pub fn generateKey( key_usages: []const []const u8, exec: *const Execution, ) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; switch (algo) { .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, exec), .aes_key_gen => |params| { @@ -87,7 +87,7 @@ fn generateKeyFromName( exec: *const Execution, ) !js.Promise { return _generateKeyFromName(name, extractable, key_usages, exec) catch |err| { - return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } }); + return exec.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } }); }; } @@ -147,7 +147,7 @@ pub fn exportKey( key: *CryptoKey, exec: *const Execution, ) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (!key.canExportKey()) { return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); } @@ -175,7 +175,7 @@ pub fn deriveBits( length: usize, exec: *const Execution, ) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; return switch (algo) { .ecdh_or_x25519 => |params| { const name = params.name; @@ -213,7 +213,7 @@ pub fn sign( .hmac => return HMAC.sign(algo, key, data, exec), else => { log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type }); - return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return exec.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); }, }; } @@ -227,7 +227,7 @@ pub fn verify( data: []const u8, // ArrayBuffer. exec: *const Execution, ) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (!algo.isHMAC()) { return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); } @@ -240,7 +240,7 @@ pub fn verify( /// Generates a digest of the given data, using the specified hash function. pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (algo.len > 10) { return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); diff --git a/src/browser/webapi/Timers.zig b/src/browser/webapi/Timers.zig index 1793b2a6..f3ac1c61 100644 --- a/src/browser/webapi/Timers.zig +++ b/src/browser/webapi/Timers.zig @@ -108,7 +108,7 @@ pub fn schedule( }; gop.value_ptr.* = callback; - try exec.context.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{ + try exec.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{ .name = opts.name, .low_priority = opts.low_priority, .finalizer = ScheduleCallback.cancelled, @@ -135,7 +135,7 @@ pub const LegacyHandler = union(enum) { switch (handler) { .function => |fun| return fun, .string => |str| { - const fun = try exec.context.local.?.compileFunction(str, &.{}, &.{}); + const fun = try exec.js.local.?.compileFunction(str, &.{}, &.{}); return fun.temp(); }, } @@ -182,7 +182,7 @@ const ScheduleCallback = struct { } var ls: js.Local.Scope = undefined; - self.exec.context.localScope(&ls); + self.exec.js.localScope(&ls); defer ls.deinit(); switch (self.mode) { @@ -195,7 +195,7 @@ const ScheduleCallback = struct { .animation_frame => { // requestAnimationFrame is window-only; if a worker ever // schedules with this mode it's a programming error. - const window = switch (self.exec.context.global) { + const window = switch (self.exec.js.global) { .frame => |frame| frame.window, .worker => unreachable, }; diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index ba8a6083..6ca775ff 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -251,7 +251,7 @@ pub fn createObjectURL(blob: *Blob, exec: *const Execution) ![]const u8 { var uuid_buf: [36]u8 = undefined; @import("../../id.zig").uuidv4(&uuid_buf); - switch (exec.context.global) { + switch (exec.js.global) { inline else => |g| { const blob_url = try std.fmt.allocPrint( g.arena, @@ -271,7 +271,7 @@ pub fn revokeObjectURL(url: []const u8, exec: *const Execution) void { return; } - switch (exec.context.global) { + switch (exec.js.global) { inline else => |g| { if (g._blob_urls.fetchRemove(url)) |entry| { entry.value.releaseRef(g._page); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 6e8be504..f6bef76c 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -39,6 +39,7 @@ const Event = @import("Event.zig"); const EventTarget = @import("EventTarget.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); const MessageEvent = @import("event/MessageEvent.zig"); +const MessagePort = @import("MessagePort.zig"); const MediaQueryList = @import("css/MediaQueryList.zig"); const storage = @import("storage/storage.zig"); const Element = @import("Element.zig"); @@ -168,6 +169,42 @@ pub fn getConsole(self: *Window) *Console { return &self._console; } +pub fn setConsole(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "console"); +} + +pub fn setSelf(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "self"); +} + +pub fn setFrames(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "frames"); +} + +pub fn setParent(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "parent"); +} + +pub fn setLength(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "length"); +} + +pub fn setScrollX(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "scrollX"); +} + +pub fn setScrollY(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "scrollY"); +} + +pub fn setPageXOffset(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "pageXOffset"); +} + +pub fn setPageYOffset(_: *Window, value: js.Value) void { + replaceGlobalProperty(value, "pageYOffset"); +} + pub fn getNavigator(self: *Window) *Navigator { return &self._navigator; } @@ -561,7 +598,7 @@ pub fn close(self: *Window) void { }; } -pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void { +pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void { // For now, we ignore targetOrigin checking and just dispatch the message // In a full implementation, we would validate the origin _ = target_origin; @@ -581,6 +618,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons .frame = target_frame, .source = source_window, .origin = try arena.dupe(u8, origin), + .ports = if (transfer) |t| try arena.dupe(*MessagePort, t) else &.{}, }; try target_frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{ @@ -763,6 +801,14 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js. } } +// `console` and a handful of other Window attributes are [Replaceable]: assigning +// to them redefines the attribute as an own data property on the global instead +// of throwing (which a getter-only accessor does in strict mode / modules). +fn replaceGlobalProperty(value: js.Value, comptime name: []const u8) void { + const global = value.local.getGlobal(); + _ = global.defineOwnProperty(name, value, 0); +} + pub const Access = union(enum) { window: *Window, cross_origin: *CrossOriginWindow, @@ -788,6 +834,7 @@ const PostMessageCallback = struct { arena: Allocator, origin: []const u8, message: js.Value.Temp, + ports: []const *MessagePort, fn deinit(self: *PostMessageCallback) void { self.frame.releaseArena(self.arena); @@ -811,6 +858,7 @@ const PostMessageCallback = struct { .data = .{ .value = self.message }, .origin = self.origin, .source = self.source, + .ports = self.ports, .bubbles = false, .cancelable = false, }, frame._page)).asEvent(); @@ -861,12 +909,12 @@ pub const JsApi = struct { }; pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 }, .deletable = false }); - pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } }); + pub const console = bridge.accessor(Window.getConsole, Window.setConsole, .{}); pub const top = bridge.accessor(Window.getTop, null, .{}); - pub const self = bridge.accessor(Window.getWindow, null, .{}); + pub const self = bridge.accessor(Window.getWindow, Window.setSelf, .{}); pub const window = bridge.accessor(Window.getWindow, null, .{}); - pub const parent = bridge.accessor(Window.getParent, null, .{}); + pub const parent = bridge.accessor(Window.getParent, Window.setParent, .{}); pub const navigator = bridge.accessor(Window.getNavigator, null, .{}); pub const screen = bridge.accessor(Window.getScreen, null, .{}); pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{}); @@ -910,13 +958,13 @@ pub const JsApi = struct { pub const getSelection = bridge.function(Window.getSelection, .{}); pub const frameElement = bridge.accessor(Window.getFrameElement, null, .{}); - pub const frames = bridge.accessor(Window.getWindow, null, .{}); + pub const frames = bridge.accessor(Window.getWindow, Window.setFrames, .{}); pub const index = bridge.indexed(Window.getFrame, null, .{ .null_as_undefined = true }); - pub const length = bridge.accessor(Window.getFramesLength, null, .{}); - pub const scrollX = bridge.accessor(Window.getScrollX, null, .{}); - pub const scrollY = bridge.accessor(Window.getScrollY, null, .{}); - pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{}); - pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{}); + pub const length = bridge.accessor(Window.getFramesLength, Window.setLength, .{}); + pub const scrollX = bridge.accessor(Window.getScrollX, Window.setScrollX, .{}); + pub const scrollY = bridge.accessor(Window.getScrollY, Window.setScrollY, .{}); + pub const pageXOffset = bridge.accessor(Window.getScrollX, Window.setPageXOffset, .{}); + pub const pageYOffset = bridge.accessor(Window.getScrollY, Window.setPageYOffset, .{}); pub const scrollTo = bridge.function(Window.scrollTo, .{}); pub const scroll = bridge.function(Window.scrollTo, .{}); pub const scrollBy = bridge.function(Window.scrollBy, .{}); @@ -927,9 +975,10 @@ pub const JsApi = struct { // sites not to try to access those features pub const isSecureContext = bridge.property(false, .{ .template = false }); - pub const innerWidth = bridge.property(1920, .{ .template = false }); - pub const innerHeight = bridge.property(1080, .{ .template = false }); - pub const devicePixelRatio = bridge.property(1, .{ .template = false }); + // [Replaceable] (CSSOM-View): writable so assignment overwrites rather than throws. + pub const innerWidth = bridge.property(1920, .{ .template = false, .readonly = false }); + pub const innerHeight = bridge.property(1080, .{ .template = false, .readonly = false }); + pub const devicePixelRatio = bridge.property(1, .{ .template = false, .readonly = false }); pub const opener = bridge.accessor(Window.getOpener, null, .{}); pub const closed = bridge.accessor(Window.getClosed, null, .{}); @@ -987,8 +1036,8 @@ pub const JsApi = struct { const CrossOriginWindow = struct { window: *Window, - pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void { - return self.window.postMessage(message, target_origin, frame); + pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void { + return self.window.postMessage(message, target_origin, transfer, frame); } pub fn getTop(self: *CrossOriginWindow, frame: *Frame) Access { diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index 4c96a0e4..be698018 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -25,7 +25,6 @@ const URL = @import("../URL.zig"); const Frame = @import("../Frame.zig"); const HttpClient = @import("../HttpClient.zig"); -const Blob = @import("Blob.zig"); const EventTarget = @import("EventTarget.zig"); const MessageEvent = @import("event/MessageEvent.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); @@ -89,16 +88,6 @@ pub fn init(url: []const u8, frame: *Frame) !*Worker { return self; } - if (std.mem.startsWith(u8, url, "blob:")) { - errdefer frame.removeWorker(self); - const blob: *Blob = frame.lookupBlobUrl(url) orelse { - log.warn(.js, "invalid blob", .{ .target = "worker" }); - return error.BlobNotFound; - }; - try self.loadInitialScript(blob._slice); - return self; - } - const headers = try session.browser.http_client.newHeaders(); frame.makeRequest(.{ .ctx = self, diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 7744eca0..621993af 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -144,6 +144,8 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { }); errdefer factory.destroy(self); + self._http_owner.blob_urls = &self._blob_urls; + self._script_manager = ScriptManagerBase.init( arena, &session.browser.http_client, @@ -231,10 +233,6 @@ pub fn isSameOrigin(self: *const WorkerGlobalScope, url: [:0]const u8) bool { return std.mem.eql(u8, URL.getHost(url), URL.getHost(current_origin)); } -pub fn lookupBlobUrl(self: *WorkerGlobalScope, url: []const u8) ?*Blob { - return self._blob_urls.get(url); -} - pub fn makeRequest(self: *WorkerGlobalScope, req: HttpClient.Request) !void { return self._session.browser.http_client.request(req, &self._http_owner); } @@ -247,6 +245,14 @@ pub fn getConsole(self: *WorkerGlobalScope) *Console { return &self._console; } +pub fn setConsole(_: *WorkerGlobalScope, value: JS.Value) void { + replaceGlobalProperty(value, "console"); +} + +pub fn setSelf(_: *WorkerGlobalScope, value: JS.Value) void { + replaceGlobalProperty(value, "self"); +} + pub fn getCrypto(self: *WorkerGlobalScope) *Crypto { return &self._crypto; } @@ -556,6 +562,14 @@ pub fn clearInterval(self: *WorkerGlobalScope, id: u32) void { self._timers.clear(id); } +// `console` and `self` are [Replaceable] assignment redefines them as own data +// properties on the global rather than throwing through the getter-only accessor +// in strict mode. +fn replaceGlobalProperty(value: JS.Value, comptime name: []const u8) void { + const global = value.local.getGlobal(); + _ = global.defineOwnProperty(name, value, 0); +} + const FunctionSetter = union(enum) { func: JS.Function.Global, anything: JS.Value, @@ -632,8 +646,8 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const self = bridge.accessor(WorkerGlobalScope.getSelf, null, .{}); - pub const console = bridge.accessor(WorkerGlobalScope.getConsole, null, .{}); + pub const self = bridge.accessor(WorkerGlobalScope.getSelf, WorkerGlobalScope.setSelf, .{}); + pub const console = bridge.accessor(WorkerGlobalScope.getConsole, WorkerGlobalScope.setConsole, .{}); pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{}); pub const performance = bridge.accessor(struct { // Unnecessary, But, our WebAPI getters are ALWAYS `fn getPerformance()...`. diff --git a/src/browser/webapi/canvas/OffscreenCanvas.zig b/src/browser/webapi/canvas/OffscreenCanvas.zig index 96363756..f3e113a9 100644 --- a/src/browser/webapi/canvas/OffscreenCanvas.zig +++ b/src/browser/webapi/canvas/OffscreenCanvas.zig @@ -73,8 +73,8 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, exec: *Executio /// Returns a Promise that resolves to a Blob containing the image. /// Since we have no actual rendering, this returns an empty blob. pub fn convertToBlob(_: *OffscreenCanvas, exec: *Execution) !js.Promise { - const blob = try Blob.init(null, null, exec.context.page); - return exec.context.local.?.resolvePromise(blob); + const blob = try Blob.init(null, null, exec.page); + return exec.js.local.?.resolvePromise(blob); } /// Returns an ImageBitmap with the rendered content (stub). diff --git a/src/browser/webapi/crypto/HMAC.zig b/src/browser/webapi/crypto/HMAC.zig index 3ab0e507..89277177 100644 --- a/src/browser/webapi/crypto/HMAC.zig +++ b/src/browser/webapi/crypto/HMAC.zig @@ -35,7 +35,7 @@ pub fn init( key_usages: []const []const u8, exec: *const Execution, ) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; // Per spec, an unrecognized hash is caught during algorithm normalization // and surfaces as NotSupportedError. const digest = crypto.findDigest(switch (params.hash) { @@ -99,7 +99,7 @@ pub fn sign( data: []const u8, exec: *const Execution, ) !js.Promise { - var resolver = exec.context.local.?.createPromiseResolver(); + var resolver = exec.js.local.?.createPromiseResolver(); if (!algo.isHMAC() or !crypto_key.canSign()) { resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); @@ -135,7 +135,7 @@ pub fn verify( data: []const u8, exec: *const Execution, ) !js.Promise { - var resolver = exec.context.local.?.createPromiseResolver(); + var resolver = exec.js.local.?.createPromiseResolver(); if (!crypto_key.canVerify()) { resolver.rejectError("HMAC.verify", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); diff --git a/src/browser/webapi/crypto/X25519.zig b/src/browser/webapi/crypto/X25519.zig index aaa1798c..048ab999 100644 --- a/src/browser/webapi/crypto/X25519.zig +++ b/src/browser/webapi/crypto/X25519.zig @@ -37,7 +37,7 @@ pub fn init( // gather them together with a single alloc call. Not sure if factory // pattern is suitable for it though. - const local = exec.context.local.?; + const local = exec.js.local.?; // Calculate usages; only matters for private key. // Only deriveKey() and deriveBits() be used for X25519. diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index d4a649e3..2bd07147 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -35,11 +35,13 @@ _proto: *Event, _data: ?Data = null, _origin: []const u8 = "", _source: ?*Window = null, +_ports: []const *MessagePort = &.{}, const MessageEventOptions = struct { data: ?Data = null, origin: ?[]const u8 = null, source: ?*Window = null, + ports: []const *MessagePort = &.{}, }; pub const Data = union(enum) { @@ -75,6 +77,7 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool ._data = opts.data, ._origin = if (opts.origin) |str| try arena.dupe(u8, str) else "", ._source = opts.source, + ._ports = if (opts.ports.len == 0) &.{} else try arena.dupe(*MessagePort, opts.ports), }, ); @@ -117,8 +120,8 @@ pub fn getSource(self: *const MessageEvent) ?*Window { return self._source; } -pub fn getPorts(_: *const MessageEvent) []*MessagePort { - return &.{}; +pub fn getPorts(self: *const MessageEvent) []const *MessagePort { + return self._ports; } pub const JsApi = struct { diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index a3576535..1f69ef0e 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -24,7 +24,6 @@ const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const URL = @import("../../URL.zig"); -const Blob = @import("../Blob.zig"); const Request = @import("Request.zig"); const Response = @import("Response.zig"); const AbortSignal = @import("../AbortSignal.zig"); @@ -48,7 +47,7 @@ pub const Input = Request.Input; pub const InitOpts = Request.InitOpts; pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promise { - const resolver = exec.context.local.?.createPromiseResolver(); + const resolver = exec.js.local.?.createPromiseResolver(); // A bad RequestInit (e.g. an invalid priority) must reject the promise, // not throw synchronously. @@ -64,12 +63,8 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis } } - if (std.mem.startsWith(u8, request._url, "blob:")) { - return handleBlobUrl(request._url, resolver, exec); - } - const response = try Response.init(null, .{ .status = 0 }, exec); - errdefer response.deinit(exec.context.page); + errdefer response.deinit(exec.page); const fetch = try response._arena.create(Fetch); fetch.* = .{ @@ -82,7 +77,7 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis ._signal = request._signal, }; - const session = exec.context.page.session; + const session = exec.session; const http_client = &session.browser.http_client; var headers = try http_client.newHeaders(); if (request._headers) |h| { @@ -127,26 +122,6 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis return resolver.promise(); } -fn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, exec: *const Execution) !js.Promise { - const blob: *Blob = exec.lookupBlobUrl(url) orelse { - resolver.rejectError("fetch blob error", .{ .type_error = "BlobNotFound" }); - return resolver.promise(); - }; - - const response = try Response.init(null, .{ .status = 200 }, exec); - response._body = .{ .bytes = try response._arena.dupe(u8, blob._slice) }; - response._url = try response._arena.dupeZ(u8, url); - response._type = .basic; - - if (blob._mime.len > 0) { - try response._headers.append("Content-Type", blob._mime, exec); - } - - const js_val = try exec.context.local.?.zigValueToJs(response, .{}); - resolver.resolve("fetch blob done", js_val); - return resolver.promise(); -} - fn httpStartCallback(response: HttpClient.Response) !void { const self: *Fetch = @ptrCast(@alignCast(response.ctx)); if (comptime IS_DEBUG) { @@ -238,7 +213,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void { }); var ls: js.Local.Scope = undefined; - self._exec.context.localScope(&ls); + self._exec.js.localScope(&ls); defer ls.deinit(); const js_val = try ls.local.zigValueToJs(self._response, .{}); @@ -269,11 +244,11 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { // clear this. (defer since `self is in the response's arena). defer if (owns_response) { - response.deinit(self._exec.context.page); + response.deinit(self._exec.page); }; var ls: js.Local.Scope = undefined; - self._exec.context.localScope(&ls); + self._exec.js.localScope(&ls); defer ls.deinit(); // fetch() must reject with a TypeError on network errors per spec @@ -286,7 +261,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void { if (self._owns_response) { var response = self._response; response._http_response = null; - response.deinit(self._exec.context.page); + response.deinit(self._exec.page); // Do not access `self` after this point: the Fetch struct was // allocated from response._arena which has been released. } diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index 12f0ecf6..5fd493a8 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -68,7 +68,7 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD }); }; - const frame = switch (exec.context.global) { + const frame = switch (exec.js.global) { .frame => |f| f, .worker => lp.assert(false, "FormData worker form", .{}), }; diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index 8954b4b2..1c0854cf 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -214,7 +214,7 @@ fn consume(self: *Request, local: *const js.Local) ?js.Promise { } pub fn blob(self: *Request, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -223,12 +223,12 @@ pub fn blob(self: *Request, exec: *const Execution) !js.Promise { const headers = try self.getHeaders(exec); const content_type = try headers.get("content-type", exec) orelse ""; - const b = try Blob.initFromBytes(body, content_type, true, exec.context.page); + const b = try Blob.initFromBytes(body, content_type, true, exec.page); return local.resolvePromise(b); } pub fn text(self: *Request, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -236,7 +236,7 @@ pub fn text(self: *Request, exec: *const Execution) !js.Promise { } pub fn json(self: *Request, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -248,7 +248,7 @@ pub fn json(self: *Request, exec: *const Execution) !js.Promise { } pub fn arrayBuffer(self: *Request, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -256,7 +256,7 @@ pub fn arrayBuffer(self: *Request, exec: *const Execution) !js.Promise { } pub fn bytes(self: *Request, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index a4986efb..b68149fc 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -70,7 +70,7 @@ const InitOpts = struct { pub const BodyInit = body_init.BodyInit; pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response { - const session = exec.context.page.session; + const session = exec.session; const arena = try session.getArena(.large, "Response"); errdefer session.releaseArena(arena); @@ -191,7 +191,7 @@ fn consume(self: *Response, local: *const js.Local) ?js.Promise { } pub fn getText(self: *Response, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -205,7 +205,7 @@ pub fn getText(self: *Response, exec: *const Execution) !js.Promise { } pub fn getJson(self: *Response, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -222,7 +222,7 @@ pub fn getJson(self: *Response, exec: *const Execution) !js.Promise { } pub fn arrayBuffer(self: *Response, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| { return rejected; } @@ -246,7 +246,7 @@ const StreamConsumer = struct { resolver: js.PromiseResolver.Global, fn start(stream: *ReadableStream, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; var resolver = local.createPromiseResolver(); const promise = resolver.promise(); @@ -267,7 +267,7 @@ const StreamConsumer = struct { } fn pumpRead(self: *StreamConsumer) !void { - const local = self.execution.context.local.?; + const local = self.execution.js.local.?; const read_promise = try self.reader.read(self.execution); const then_fn = local.newCallback(onReadFulfilled, self); @@ -284,7 +284,7 @@ const StreamConsumer = struct { }; fn onReadFulfilled(self: *StreamConsumer, data_: ?ReadData) void { - const local = self.execution.context.local.?; + const local = self.execution.js.local.?; const data = data_ orelse { return self.finish(local, null); @@ -297,7 +297,7 @@ const StreamConsumer = struct { fn _onReadFulfilled(self: *StreamConsumer, data: ReadData) !void { const exec = self.execution; - const local = exec.context.local.?; + const local = exec.js.local.?; if (data.done) { // Stream is finished, concatenate all chunks and resolve @@ -328,7 +328,7 @@ const StreamConsumer = struct { } fn onReadRejected(self: *StreamConsumer) void { - self.finish(self.execution.context.local.?, null); + self.finish(self.execution.js.local.?, null); } fn concatenateChunks(self: *StreamConsumer, allocator: Allocator) ![]const u8 { @@ -348,7 +348,7 @@ const StreamConsumer = struct { }; pub fn blob(self: *Response, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| return rejected; const body = switch (self._body) { .bytes => |b| b, @@ -356,12 +356,12 @@ pub fn blob(self: *Response, exec: *const Execution) !js.Promise { .stream => return local.rejectPromise(.{ .type_error = "Cannot read blob from stream body" }), }; const content_type = try self._headers.get("content-type", exec) orelse ""; - const b = try Blob.initFromBytes(body, content_type, true, exec.context.page); + const b = try Blob.initFromBytes(body, content_type, true, exec.page); return local.resolvePromise(b); } pub fn bytes(self: *Response, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self.consume(local)) |rejected| return rejected; const body = switch (self._body) { .bytes => |b| b, @@ -372,7 +372,7 @@ pub fn bytes(self: *Response, exec: *const Execution) !js.Promise { } pub fn clone(self: *const Response, exec: *const Execution) !*Response { - const session = exec.context.page.session; + const session = exec.session; const body_len = switch (self._body) { .bytes => |b| b.len, .empty => 0, diff --git a/src/browser/webapi/net/WebSocket.zig b/src/browser/webapi/net/WebSocket.zig index f033fb44..4d2b589f 100644 --- a/src/browser/webapi/net/WebSocket.zig +++ b/src/browser/webapi/net/WebSocket.zig @@ -117,7 +117,7 @@ pub fn init(url: []const u8, protocols: [][]const u8, exec: *const Execution) !* const resolved_url = try URL.resolve(arena, exec.base(), url, .{ .always_dupe = true, .encoding = exec.charset.* }); - const http_client = &exec.context.page.session.browser.http_client; + const http_client = &exec.session.browser.http_client; const conn = http_client.network.newConnection() orelse { return error.NoFreeConnection; }; @@ -240,7 +240,7 @@ fn cleanup(self: *WebSocket) void { self._http_client.removeConn(conn); self._req_headers.deinit(); self._conn = null; - self.releaseRef(self._exec.context.page); + self.releaseRef(self._exec.page); self._send_queue.clearRetainingCapacity(); } } @@ -457,7 +457,7 @@ fn dispatchOpenEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (exec.hasDirectListeners(target, "open", self._on_open)) { - const event = try Event.initTrusted(comptime .wrap("open"), .{}, exec.context.page); + const event = try Event.initTrusted(comptime .wrap("open"), .{}, exec.page); try exec.dispatch(target, event, self._on_open, .{ .context = "WebSocket open" }); } } @@ -471,7 +471,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF switch (self._binary_type) { .arraybuffer => .{ .arraybuffer = .{ .values = data } }, .blob => blk: { - const blob = try Blob.initFromBytes(data, "", false, exec.context.page); + const blob = try Blob.initFromBytes(data, "", false, exec.page); blob.acquireRef(); break :blk .{ .blob = blob }; }, @@ -482,7 +482,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF const event = try MessageEvent.initTrusted(comptime .wrap("message"), .{ .data = msg_data, .origin = "", - }, exec.context.page); + }, exec.page); try exec.dispatch(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" }); } } @@ -492,7 +492,7 @@ fn dispatchErrorEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (exec.hasDirectListeners(target, "error", self._on_error)) { - const event = try Event.initTrusted(comptime .wrap("error"), .{}, exec.context.page); + const event = try Event.initTrusted(comptime .wrap("error"), .{}, exec.page); try exec.dispatch(target, event, self._on_error, .{ .context = "WebSocket error" }); } } @@ -506,7 +506,7 @@ fn dispatchCloseEvent(self: *WebSocket, code: u16, reason: []const u8, was_clean .code = code, .reason = reason, .wasClean = was_clean, - }, exec.context.page); + }, exec.page); try exec.dispatch(target, event.asEvent(), self._on_close, .{ .context = "WebSocket close" }); } } @@ -580,7 +580,7 @@ fn writeContent(self: *WebSocket, conn: *http.Connection, buf: []u8, byte_msg: M if (self._send_offset >= byte_msg.data.len) { const removed = self._send_queue.orderedRemove(0); - removed.deinit(self._exec.context.page); + removed.deinit(self._exec.page); if (comptime IS_DEBUG) { log.debug(.websocket, "send complete", .{ .url = self._url, .len = byte_msg.data.len, .queue = self._send_queue.items.len }); } diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 98027202..9dd5de4b 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -146,7 +146,7 @@ fn releaseSelfRef(self: *XMLHttpRequest) void { return; } self._active_request = false; - self.releaseRef(self._exec.context.page); + self.releaseRef(self._exec.page); } pub fn releaseRef(self: *XMLHttpRequest, page: *Page) void { @@ -249,11 +249,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v const exec = self._exec; - if (std.mem.startsWith(u8, self._url, "blob:")) { - return self.handleBlobUrl(exec); - } - - const session = exec.context.page.session; + const session = exec.session; const http_client = &session.browser.http_client; var headers = try http_client.newHeaders(); @@ -293,38 +289,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v }; } -fn handleBlobUrl(self: *XMLHttpRequest, exec: *const Execution) !void { - const blob = exec.lookupBlobUrl(self._url) orelse { - self.handleError(error.BlobNotFound); - return; - }; - - self._response_status = 200; - self._response_url = self._url; - - try self._response_data.appendSlice(self._arena, blob._slice); - self._response_len = blob._slice.len; - - try self.stateChanged(.headers_received, exec); - try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, exec); - try self.stateChanged(.loading, exec); - try self._proto.dispatch(.progress, .{ - .total = self._response_len orelse 0, - .loaded = self._response_data.items.len, - }, exec); - try self.stateChanged(.done, exec); - - const loaded = self._response_data.items.len; - try self._proto.dispatch(.load, .{ - .total = loaded, - .loaded = loaded, - }, exec); - try self._proto.dispatch(.load_end, .{ - .total = loaded, - .loaded = loaded, - }, exec); -} - pub fn getReadyState(self: *const XMLHttpRequest) u32 { return @intFromEnum(self._ready_state); } @@ -403,13 +367,13 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response { const res: Response = switch (self._response_type) { .text => .{ .text = data }, .json => blk: { - const value = try exec.context.local.?.parseJSON(data); + const value = try exec.js.local.?.parseJSON(data); break :blk .{ .json = try value.persist() }; }, .document => blk: { // responseType=document is only meaningful in a Frame; workers // have no DOM. Drastically different impls -> switch on global. - switch (exec.context.global) { + switch (exec.js.global) { .frame => |frame| { const document = try exec._factory.node(Node.Document{ ._proto = undefined, ._type = .generic }); try frame.parseHtmlAsChildren(document.asNode(), data); @@ -485,7 +449,7 @@ fn httpHeaderDoneCallback(response: HttpClient.Response) !bool { const exec = self._exec; var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); try self.stateChanged(.headers_received, exec); @@ -606,7 +570,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, exec: *const Execution const target = self.asEventTarget(); if (exec.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) { - const event = try Event.initTrusted(.wrap("readystatechange"), .{}, exec.context.page); + const event = try Event.initTrusted(.wrap("readystatechange"), .{}, exec.page); try exec.dispatch(target, event, self._on_ready_state_change, .{ .context = "XHR state change" }); } } diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index 7cbedc70..af5fd38f 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -61,7 +61,7 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT const event = (try ProgressEvent.initTrusted( comptime .wrap(typ), .{ .total = progress.total, .loaded = progress.loaded }, - exec.context.page, + exec.page, )).asEvent(); return exec.dispatch( diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig index 809d95af..1089d750 100644 --- a/src/browser/webapi/streams/ReadableStream.zig +++ b/src/browser/webapi/streams/ReadableStream.zig @@ -145,9 +145,9 @@ pub fn callPullIfNeeded(self: *ReadableStream) !void { const exec = self._execution; if (comptime IS_DEBUG) { - if (exec.context.local == null) { + if (exec.js.local == null) { log.fatal(.bug, "null context scope", .{ .src = "ReadableStream.callPullIfNeeded", .url = exec.url.* }); - std.debug.assert(exec.context.local != null); + std.debug.assert(exec.js.local != null); } } @@ -155,7 +155,7 @@ pub fn callPullIfNeeded(self: *ReadableStream) !void { const func = self._pull_fn orelse return; var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); // Call the pull function @@ -187,7 +187,7 @@ fn shouldCallPull(self: *const ReadableStream) bool { } pub fn cancel(self: *ReadableStream, reason: ?[]const u8, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; if (self._state != .readable) { if (self._cancel) |c| { @@ -257,10 +257,10 @@ pub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, exec: *const /// Returns a promise that resolves when piping is complete. pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, exec: *const Execution) !js.Promise { if (self.getLocked()) { - return exec.context.local.?.rejectPromise(.{ .type_error = "ReadableStream is locked" }); + return exec.js.local.?.rejectPromise(.{ .type_error = "ReadableStream is locked" }); } - const local = exec.context.local.?; + const local = exec.js.local.?; var pipe_resolver = local.createPromiseResolver(); const promise = pipe_resolver.promise(); const persisted_resolver = try pipe_resolver.persist(); @@ -295,7 +295,7 @@ const PipeState = struct { fn pumpRead(state: *PipeState) !void { const exec = state.execution; - const local = exec.context.local.?; + const local = exec.js.local.?; // Call reader.read() which returns a Promise const read_promise = try state.reader.read(exec); @@ -315,7 +315,7 @@ const PipeState = struct { }; fn onReadFulfilled(self: *PipeState, data_: ?ReadData) void { const exec = self.execution; - const local = exec.context.local.?; + const local = exec.js.local.?; const data = data_ orelse { return self.finish(local); }; @@ -346,7 +346,7 @@ const PipeState = struct { } fn onReadRejected(self: *PipeState) void { - self.finish(self.execution.context.local.?); + self.finish(self.execution.js.local.?); } fn finish(self: *PipeState, local: *const js.Local) void { @@ -399,7 +399,7 @@ pub const AsyncIterator = struct { pub fn @"return"(self: *AsyncIterator, exec: *const Execution) !js.Promise { self._reader.releaseLock(); - return exec.context.local.?.resolvePromise(.{ .done = true, .value = null }); + return exec.js.local.?.resolvePromise(.{ .done = true, .value = null }); } pub const JsApi = struct { diff --git a/src/browser/webapi/streams/ReadableStreamDefaultController.zig b/src/browser/webapi/streams/ReadableStreamDefaultController.zig index 9d258676..feda0705 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultController.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultController.zig @@ -64,7 +64,7 @@ pub fn init(stream: *ReadableStream, high_water_mark: u32, exec: *const Executio } pub fn addPendingRead(self: *ReadableStreamDefaultController) !js.Promise { - const resolver = self._execution.context.local.?.createPromiseResolver(); + const resolver = self._execution.js.local.?.createPromiseResolver(); try self._pending_reads.append(self._arena, try resolver.persist()); return resolver.promise(); } @@ -89,14 +89,14 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void { }; if (comptime IS_DEBUG) { - if (exec.context.local == null) { + if (exec.js.local == null) { log.fatal(.bug, "null context scope", .{ .src = "ReadableStreamDefaultController.enqueue", .url = exec.url.* }); - std.debug.assert(exec.context.local != null); + std.debug.assert(exec.js.local != null); } } var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); ls.toLocal(resolver).resolve("stream enqueue", result); @@ -124,14 +124,14 @@ pub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !vo }; if (comptime IS_DEBUG) { - if (exec.context.local == null) { + if (exec.js.local == null) { log.fatal(.bug, "null context scope", .{ .src = "ReadableStreamDefaultController.enqueueValue", .url = exec.url.* }); - std.debug.assert(exec.context.local != null); + std.debug.assert(exec.js.local != null); } } var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); ls.toLocal(resolver).resolve("stream enqueue value", result); @@ -152,15 +152,15 @@ pub fn close(self: *ReadableStreamDefaultController) !void { const exec = self._execution; if (comptime IS_DEBUG) { - if (exec.context.local == null) { + if (exec.js.local == null) { log.fatal(.bug, "null context scope", .{ .src = "ReadableStreamDefaultController.close", .url = exec.url.* }); - std.debug.assert(exec.context.local != null); + std.debug.assert(exec.js.local != null); } } for (self._pending_reads.items) |resolver| { var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); ls.toLocal(resolver).resolve("stream close", result); } @@ -178,7 +178,7 @@ pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void { // Reject all pending reads for (self._pending_reads.items) |resolver| { - self._execution.context.toLocal(resolver).reject("stream error", err); + self._execution.js.toLocal(resolver).reject("stream error", err); } self._pending_reads.clearRetainingCapacity(); } diff --git a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig index 0f0c8bf4..afbe85e4 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig @@ -58,7 +58,7 @@ pub const ReadResult = struct { }; pub fn read(self: *ReadableStreamDefaultReader, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; const stream = self._stream orelse { return local.rejectPromise(.{ .type_error = "Reader has been released" }); }; @@ -97,7 +97,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void { pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, exec: *const Execution) !js.Promise { const stream = self._stream orelse { - return exec.context.local.?.rejectPromise(.{ .type_error = "Reader has been released" }); + return exec.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" }); }; self.releaseLock(); diff --git a/src/browser/webapi/streams/TransformStream.zig b/src/browser/webapi/streams/TransformStream.zig index f46f9d7c..d807b2f5 100644 --- a/src/browser/webapi/streams/TransformStream.zig +++ b/src/browser/webapi/streams/TransformStream.zig @@ -94,7 +94,7 @@ pub fn transformWrite(self: *TransformStream, chunk: js.Value, exec: *const Exec if (self._controller._transform_fn) |transform_fn| { var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); try ls.toLocal(transform_fn).call(void, .{ chunk, self._controller }); @@ -106,7 +106,7 @@ pub fn transformWrite(self: *TransformStream, chunk: js.Value, exec: *const Exec pub fn transformClose(self: *TransformStream, exec: *const Execution) !void { if (self._controller._flush_fn) |flush_fn| { var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); try ls.toLocal(flush_fn).call(void, .{self._controller}); diff --git a/src/browser/webapi/streams/WritableStream.zig b/src/browser/webapi/streams/WritableStream.zig index 40bdc919..ddf3da7e 100644 --- a/src/browser/webapi/streams/WritableStream.zig +++ b/src/browser/webapi/streams/WritableStream.zig @@ -111,7 +111,7 @@ pub fn writeChunk(self: *WritableStream, chunk: js.Value, exec: *const Execution if (self._write_fn) |write_fn| { var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); try ls.toLocal(write_fn).call(void, .{ chunk, self._controller }); @@ -129,7 +129,7 @@ pub fn closeStream(self: *WritableStream, exec: *const Execution) !void { if (self._close_fn) |close_fn| { var ls: js.Local.Scope = undefined; - exec.context.localScope(&ls); + exec.js.localScope(&ls); defer ls.deinit(); try ls.toLocal(close_fn).call(void, .{self._controller}); diff --git a/src/browser/webapi/streams/WritableStreamDefaultWriter.zig b/src/browser/webapi/streams/WritableStreamDefaultWriter.zig index be5880ae..02d78b63 100644 --- a/src/browser/webapi/streams/WritableStreamDefaultWriter.zig +++ b/src/browser/webapi/streams/WritableStreamDefaultWriter.zig @@ -32,7 +32,7 @@ pub fn init(stream: *WritableStream, exec: *const Execution) !*WritableStreamDef } pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; const stream = self._stream orelse { return local.rejectPromise(.{ .type_error = "Writer has been released" }); }; @@ -47,7 +47,7 @@ pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, exec: *const E } pub fn close(self: *WritableStreamDefaultWriter, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; const stream = self._stream orelse { return local.rejectPromise(.{ .type_error = "Writer has been released" }); }; @@ -69,7 +69,7 @@ pub fn releaseLock(self: *WritableStreamDefaultWriter) void { } pub fn getClosed(self: *WritableStreamDefaultWriter, exec: *const Execution) !js.Promise { - const local = exec.context.local.?; + const local = exec.js.local.?; const stream = self._stream orelse { return local.rejectPromise(.{ .type_error = "Writer has been released" }); }; @@ -92,7 +92,7 @@ pub fn getDesiredSize(self: *const WritableStreamDefaultWriter) ?i32 { pub fn getReady(self: *WritableStreamDefaultWriter, exec: *const Execution) !js.Promise { _ = self; - return exec.context.local.?.resolvePromise(.{}); + return exec.js.local.?.resolvePromise(.{}); } pub const JsApi = struct { diff --git a/src/cdp/domains/webmcp.zig b/src/cdp/domains/webmcp.zig index eb591b12..188cfd10 100644 --- a/src/cdp/domains/webmcp.zig +++ b/src/cdp/domains/webmcp.zig @@ -256,7 +256,7 @@ pub fn onToolAdded( bc: *CDP.BrowserContext, event: *const Notification.ModelContextToolEvent, ) !void { - const global = event.exec.context.global; + const global = event.exec.js.global; var ls: js.Local.Scope = undefined; global.getJs().localScope(&ls); @@ -280,7 +280,7 @@ pub fn onToolRemoved( bc: *CDP.BrowserContext, event: *const Notification.ModelContextToolEvent, ) !void { - const frame_id = switch (event.exec.context.global) { + const frame_id = switch (event.exec.js.global) { inline else => |g| g._frame_id, }; try bc.cdp.sendEvent("WebMCP.toolsRemoved", .{ diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 63216a1d..ca8e74da 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -52,6 +52,8 @@ pub extern "C" fn html5ever_parse_document( reparent_children_callback: ReparentChildrenCallback, append_before_sibling_callback: AppendBeforeSiblingCallback, append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + allow_declarative_shadow: bool, ) -> () { if html.is_null() || len == 0 { return (); @@ -78,6 +80,8 @@ pub extern "C" fn html5ever_parse_document( reparent_children_callback: reparent_children_callback, append_before_sibling_callback: append_before_sibling_callback, append_based_on_parent_node_callback: append_based_on_parent_node_callback, + attach_declarative_shadow_callback: attach_declarative_shadow_callback, + allow_declarative_shadow: allow_declarative_shadow, }; let bytes = unsafe { std::slice::from_raw_parts(html, len) }; @@ -111,6 +115,8 @@ pub extern "C" fn html5ever_parse_document_with_encoding( reparent_children_callback: ReparentChildrenCallback, append_before_sibling_callback: AppendBeforeSiblingCallback, append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + allow_declarative_shadow: bool, ) -> () { if html.is_null() || len == 0 { return (); @@ -148,6 +154,8 @@ pub extern "C" fn html5ever_parse_document_with_encoding( reparent_children_callback: reparent_children_callback, append_before_sibling_callback: append_before_sibling_callback, append_based_on_parent_node_callback: append_based_on_parent_node_callback, + attach_declarative_shadow_callback: attach_declarative_shadow_callback, + allow_declarative_shadow: allow_declarative_shadow, }; // Parse directly from decoded string @@ -472,6 +480,8 @@ pub extern "C" fn html5ever_parse_fragment( reparent_children_callback: ReparentChildrenCallback, append_before_sibling_callback: AppendBeforeSiblingCallback, append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + allow_declarative_shadow: bool, ) -> () { if html.is_null() || len == 0 { return (); @@ -498,6 +508,8 @@ pub extern "C" fn html5ever_parse_fragment( reparent_children_callback: reparent_children_callback, append_before_sibling_callback: append_before_sibling_callback, append_based_on_parent_node_callback: append_based_on_parent_node_callback, + attach_declarative_shadow_callback: attach_declarative_shadow_callback, + allow_declarative_shadow: allow_declarative_shadow, }; let bytes = unsafe { std::slice::from_raw_parts(html, len) }; @@ -587,6 +599,8 @@ pub extern "C" fn html5ever_streaming_parser_create( reparent_children_callback: ReparentChildrenCallback, append_before_sibling_callback: AppendBeforeSiblingCallback, append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + allow_declarative_shadow: bool, ) -> *mut c_void { let arena = Box::new(typed_arena::Arena::new()); @@ -615,6 +629,8 @@ pub extern "C" fn html5ever_streaming_parser_create( reparent_children_callback: reparent_children_callback, append_before_sibling_callback: append_before_sibling_callback, append_based_on_parent_node_callback: append_based_on_parent_node_callback, + attach_declarative_shadow_callback: attach_declarative_shadow_callback, + allow_declarative_shadow: allow_declarative_shadow, }; // Create a parser which implements TendrilSink for streaming parsing @@ -716,6 +732,8 @@ pub extern "C" fn xml5ever_parse_document( reparent_children_callback: ReparentChildrenCallback, append_before_sibling_callback: AppendBeforeSiblingCallback, append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + allow_declarative_shadow: bool, ) -> () { if xml.is_null() || len == 0 { return (); @@ -742,6 +760,8 @@ pub extern "C" fn xml5ever_parse_document( reparent_children_callback: reparent_children_callback, append_before_sibling_callback: append_before_sibling_callback, append_based_on_parent_node_callback: append_based_on_parent_node_callback, + attach_declarative_shadow_callback: attach_declarative_shadow_callback, + allow_declarative_shadow: allow_declarative_shadow, }; let bytes = unsafe { std::slice::from_raw_parts(xml, len) }; diff --git a/src/html5ever/sink.rs b/src/html5ever/sink.rs index 371fd260..b47d8d96 100644 --- a/src/html5ever/sink.rs +++ b/src/html5ever/sink.rs @@ -62,6 +62,8 @@ pub struct Sink<'arena> { pub reparent_children_callback: ReparentChildrenCallback, pub append_before_sibling_callback: AppendBeforeSiblingCallback, pub append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback, + pub attach_declarative_shadow_callback: AttachDeclarativeShadowCallback, + pub allow_declarative_shadow: bool, } impl<'arena> TreeSink for Sink<'arena> { @@ -286,4 +288,26 @@ impl<'arena> TreeSink for Sink<'arena> { (self.reparent_children_callback)(self.ctx, *node, *new_parent); } } + + fn allow_declarative_shadow_roots(&self, _intended_parent: &Ref) -> bool { + self.allow_declarative_shadow + } + + fn attach_declarative_shadow(&self, location: &Ref, template: &Ref, attrs: &[Attribute]) -> bool { + // html5ever only calls this when shadowrootmode is "open" or "closed", + // so anything other than "open" is treated as "closed". + let mode_is_open = attrs + .iter() + .find(|a| a.name.local.as_ref() == "shadowrootmode") + .map(|a| a.value.as_ref() == "open") + .unwrap_or(true); + unsafe { + (self.attach_declarative_shadow_callback)( + self.ctx, + *location, + *template, + if mode_is_open { 1 } else { 0 }, + ) != 0 + } + } } diff --git a/src/html5ever/types.rs b/src/html5ever/types.rs index 48ed9c3e..13eb76a4 100644 --- a/src/html5ever/types.rs +++ b/src/html5ever/types.rs @@ -65,6 +65,13 @@ pub type AddAttrsIfMissingCallback = unsafe extern "C" fn( pub type GetTemplateContentsCallback = unsafe extern "C" fn(ctx: Ref, target: Ref) -> Ref; +pub type AttachDeclarativeShadowCallback = unsafe extern "C" fn( + ctx: Ref, + host: Ref, + template: Ref, + mode_is_open: u8, +) -> u8; + pub type RemoveFromParentCallback = unsafe extern "C" fn(ctx: Ref, target: Ref) -> (); pub type ReparentChildrenCallback = unsafe extern "C" fn(ctx: Ref, node: Ref, new_parent: Ref) -> (); diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig deleted file mode 100644 index bd77b5fa..00000000 --- a/src/main_legacy_test.zig +++ /dev/null @@ -1,281 +0,0 @@ -const std = @import("std"); -const lp = @import("lightpanda"); - -const Allocator = std.mem.Allocator; - -// used in custom panic handler -var current_test: ?[]const u8 = null; - -pub fn main() !void { - var gpa: std.heap.DebugAllocator(.{}) = .init; - defer _ = gpa.deinit(); - - const allocator = gpa.allocator(); - - var args = try std.process.argsWithAllocator(allocator); - defer args.deinit(); - _ = args.next(); // executable name - - var filter: ?[]const u8 = null; - if (args.next()) |n| { - filter = n; - } - - var http_server = try TestHTTPServer.init(); - defer http_server.deinit(); - - { - var wg: std.Thread.WaitGroup = .{}; - wg.startMany(1); - var thrd = try std.Thread.spawn(.{}, TestHTTPServer.run, .{ &http_server, &wg }); - thrd.detach(); - wg.wait(); - } - lp.log.opts.level = .warn; - const config = try lp.Config.init(allocator, "legacy-test", .{ .serve = .{ - .insecure_disable_tls_host_verification = true, - .user_agent_suffix = "internal-tester", - } }); - defer config.deinit(allocator); - - var app = try lp.App.init(allocator, &config); - defer app.deinit(); - - var test_arena = std.heap.ArenaAllocator.init(allocator); - defer test_arena.deinit(); - - var browser: lp.Browser = undefined; - try browser.init(app, .{}, null); - defer browser.deinit(); - - const notification = try lp.Notification.init(allocator); - defer notification.deinit(); - - const session = try browser.newSession(notification); - defer session.deinit(); - - var dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{ .iterate = true, .no_follow = true }); - defer dir.close(); - - var walker = try dir.walk(allocator); - defer walker.deinit(); - - while (try walker.next()) |entry| { - _ = test_arena.reset(.retain_capacity); - if (entry.kind != .file) { - continue; - } - - if (!std.mem.endsWith(u8, entry.basename, ".html")) { - continue; - } - - if (std.mem.endsWith(u8, entry.basename, ".skip.html")) { - continue; - } - - if (filter) |f| { - if (std.mem.indexOf(u8, entry.path, f) == null) { - continue; - } - } - std.debug.print("\n===={s}====\n", .{entry.path}); - current_test = entry.path; - run(test_arena.allocator(), entry.path, session) catch |err| { - std.debug.print("Failure: {s} - {any}\n", .{ entry.path, err }); - }; - } -} - -pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void { - const url = try std.fmt.allocPrintSentinel(allocator, "http://localhost:9589/{s}", .{file}, 0); - - const frame = try session.createPage(); - defer session.removePage(); - - var ls: lp.js.Local.Scope = undefined; - frame.js.localScope(&ls); - defer ls.deinit(); - - var try_catch: lp.js.TryCatch = undefined; - try_catch.init(&ls.local); - defer try_catch.deinit(); - - try frame.navigate(url, .{}); - var runner = try session.runner(.{}); - try runner.wait(false, .{ .ms = 2000 }); - - ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| { - const caught = try_catch.caughtOrError(allocator, err); - std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught }); - return err; - }; -} - -const TestHTTPServer = struct { - shutdown: bool, - dir: std.fs.Dir, - listener: ?std.net.Server, - - pub fn init() !TestHTTPServer { - return .{ - .dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{}), - .shutdown = true, - .listener = null, - }; - } - - pub fn deinit(self: *TestHTTPServer) void { - self.shutdown = true; - if (self.listener) |*listener| { - listener.deinit(); - } - self.dir.close(); - } - - pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void { - const address = try std.net.Address.parseIp("127.0.0.1", 9589); - - self.listener = try address.listen(.{ .reuse_address = true }); - var listener = &self.listener.?; - - wg.finish(); - - while (true) { - const conn = listener.accept() catch |err| { - if (self.shutdown) { - return; - } - return err; - }; - const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn }); - thrd.detach(); - } - } - - fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void { - defer conn.stream.close(); - - var req_buf: [2048]u8 = undefined; - var conn_reader = conn.stream.reader(&req_buf); - var conn_writer = conn.stream.writer(&req_buf); - - var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface); - - while (true) { - var req = http_server.receiveHead() catch |err| switch (err) { - error.ReadFailed => continue, - error.HttpConnectionClosing => continue, - else => { - std.debug.print("Test HTTP Server error: {}\n", .{err}); - return err; - }, - }; - - self.handler(&req) catch |err| { - std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err }); - try req.respond("server error", .{ .status = .internal_server_error }); - return; - }; - } - } - - fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void { - const path = req.head.target; - - if (std.mem.eql(u8, path, "/xhr")) { - return req.respond("1234567890" ** 10, .{ - .extra_headers = &.{ - .{ .name = "Content-Type", .value = "text/html; charset=utf-8" }, - }, - }); - } - - if (std.mem.eql(u8, path, "/xhr/json")) { - return req.respond("{\"over\":\"9000!!!\"}", .{ - .extra_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - }, - }); - } - - // strip out leading '/' to make the path relative - const file = try server.dir.openFile(path[1..], .{}); - defer file.close(); - - const stat = try file.stat(); - var send_buffer: [4096]u8 = undefined; - - var res = try req.respondStreaming(&send_buffer, .{ - .content_length = stat.size, - .respond_options = .{ - .extra_headers = &.{ - .{ .name = "content-type", .value = getContentType(path) }, - }, - }, - }); - - var read_buffer: [4096]u8 = undefined; - var reader = file.reader(&read_buffer); - _ = try res.writer.sendFileAll(&reader, .unlimited); - try res.writer.flush(); - try res.end(); - } - - pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { - var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) { - error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), - else => return err, - }; - defer file.close(); - - const stat = try file.stat(); - var send_buffer: [4096]u8 = undefined; - - var res = try req.respondStreaming(&send_buffer, .{ - .content_length = stat.size, - .respond_options = .{ - .extra_headers = &.{ - .{ .name = "content-type", .value = getContentType(file_path) }, - }, - }, - }); - - var read_buffer: [4096]u8 = undefined; - var reader = file.reader(&read_buffer); - _ = try res.writer.sendFileAll(&reader, .unlimited); - try res.writer.flush(); - try res.end(); - } - - fn getContentType(file_path: []const u8) []const u8 { - if (std.mem.endsWith(u8, file_path, ".js")) { - return "application/json"; - } - - if (std.mem.endsWith(u8, file_path, ".html")) { - return "text/html"; - } - - if (std.mem.endsWith(u8, file_path, ".htm")) { - return "text/html"; - } - - if (std.mem.endsWith(u8, file_path, ".xml")) { - // some wpt tests do this - return "text/xml"; - } - - std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path}); - return "text/html"; - } -}; - -pub const panic = std.debug.FullPanic(struct { - pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { - if (current_test) |ct| { - std.debug.print("===panic running: {s}===\n", .{ct}); - } - std.debug.defaultPanic(msg, first_trace_addr); - } -}.panicFn); diff --git a/src/testing.zig b/src/testing.zig index f89741fe..97e86d98 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -654,6 +654,15 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { }); } + if (std.mem.eql(u8, path, "/src/browser/tests/401")) { + return req.respond("No", .{ + .status = .unauthorized, + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, + }); + } + if (std.mem.eql(u8, path, "/404.js")) { // Valid JS body served with a 404 status. Used to assert that // ScriptManager does NOT execute the body of a failed script