diff --git a/src/browser/dom/css.zig b/src/browser/dom/css.zig
index 50c262e4..c9f3d647 100644
--- a/src/browser/dom/css.zig
+++ b/src/browser/dom/css.zig
@@ -49,7 +49,7 @@ const MatchAll = struct {
fn init(alloc: std.mem.Allocator) MatchAll {
return .{
.alloc = alloc,
- .nl = NodeList.init(),
+ .nl = .{},
};
}
@@ -62,7 +62,8 @@ const MatchAll = struct {
}
fn toOwnedList(m: *MatchAll) NodeList {
- defer m.nl = NodeList.init();
+ // reset it.
+ defer m.nl = .{};
return m.nl;
}
};
diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig
index c6b57b8d..8aac8f3b 100644
--- a/src/browser/dom/mutation_observer.zig
+++ b/src/browser/dom/mutation_observer.zig
@@ -17,18 +17,17 @@
// along with this program. If not, see .
const std = @import("std");
+const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env;
-const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
MutationObserver,
MutationRecord,
- MutationRecords,
};
const Walker = @import("../dom/walker.zig").WalkerChildren;
@@ -38,109 +37,101 @@ const log = std.log.scoped(.events);
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
cbk: Env.Callback,
- observers: Observers,
+ arena: Allocator,
- const Observer = struct {
- node: *parser.Node,
- options: MutationObserverInit,
- };
+ // List of records which were observed. When the scopeEnds, we need to
+ // execute our callback with it.
+ observed: std.ArrayListUnmanaged(*MutationRecord),
- const Observers = std.ArrayListUnmanaged(*Observer);
-
- pub const MutationObserverInit = struct {
- childList: bool = false,
- attributes: bool = false,
- characterData: bool = false,
- subtree: bool = false,
- attributeOldValue: bool = false,
- characterDataOldValue: bool = false,
- // TODO
- // attributeFilter: [][]const u8,
-
- fn attr(self: MutationObserverInit) bool {
- return self.attributes or self.attributeOldValue;
- }
-
- fn cdata(self: MutationObserverInit) bool {
- return self.characterData or self.characterDataOldValue;
- }
- };
-
- pub fn constructor(cbk: Env.Callback) !MutationObserver {
- return MutationObserver{
+ pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver {
+ return .{
.cbk = cbk,
- .observers = .{},
+ .observed = .{},
+ .arena = state.arena,
};
}
- // TODO
- fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit {
- return opt orelse .{};
- }
+ pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
+ const options = options_ orelse MutationObserverInit{};
- pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void {
- const arena = state.arena;
- const o = try arena.create(Observer);
- o.* = .{
+ const observer = try self.arena.create(Observer);
+ observer.* = .{
.node = node,
- .options = resolveOptions(options),
+ .options = options,
+ .mutation_observer = self,
};
- errdefer arena.destroy(o);
- // register the new observer.
- try self.observers.append(arena, o);
+ const arena = self.arena;
- // register node's events.
- if (o.options.childList or o.options.subtree) {
- try parser.eventTargetAddEventListener(
+ // register node's events
+ if (options.childList or options.subtree) {
+ try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeInserted",
- EventHandler,
- .{ .cbk = self.cbk, .ctx = o },
+ Observer.handle,
+ observer,
false,
);
- try parser.eventTargetAddEventListener(
+ try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved",
- EventHandler,
- .{ .cbk = self.cbk, .ctx = o },
+ Observer.handle,
+ observer,
false,
);
}
- if (o.options.attr()) {
- try parser.eventTargetAddEventListener(
+ if (options.attr()) {
+ try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMAttrModified",
- EventHandler,
- .{ .cbk = self.cbk, .ctx = o },
+ Observer.handle,
+ observer,
false,
);
}
- if (o.options.cdata()) {
- try parser.eventTargetAddEventListener(
+ if (options.cdata()) {
+ try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMCharacterDataModified",
- EventHandler,
- .{ .cbk = self.cbk, .ctx = o },
+ Observer.handle,
+ observer,
false,
);
}
- if (o.options.subtree) {
- try parser.eventTargetAddEventListener(
+ if (options.subtree) {
+ try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMSubtreeModified",
- EventHandler,
- .{ .cbk = self.cbk, .ctx = o },
+ Observer.handle,
+ observer,
false,
);
}
}
+ pub fn jsScopeEnd(self: *MutationObserver, _: anytype) void {
+ const record = self.observed.items;
+ if (record.len == 0) {
+ return;
+ }
+
+ defer self.observed.clearRetainingCapacity();
+
+ for (record) |r| {
+ const records = [_]MutationRecord{r.*};
+ var result: Env.Callback.Result = undefined;
+ self.cbk.tryCall(.{records}, &result) catch {
+ log.err("mutation observer callback error: {s}", .{result.exception});
+ log.debug("stack:\n{s}", .{result.stack orelse "???"});
+ };
+ }
+ }
+
// TODO
pub fn _disconnect(_: *MutationObserver) !void {
// TODO unregister listeners.
@@ -152,50 +143,27 @@ pub const MutationObserver = struct {
}
};
-// Handle multiple record?
-pub const MutationRecords = struct {
- first: ?MutationRecord = null,
-
- pub fn get_length(self: *const MutationRecords) u32 {
- if (self.first == null) return 0;
-
- return 1;
- }
- pub fn indexed_get(self: *const MutationRecords, i: u32, has_value: *bool) ?MutationRecord {
- _ = i;
- return self.first orelse {
- has_value.* = false;
- return null;
- };
- }
- pub fn postAttach(self: *const MutationRecords, js_this: JsThis) !void {
- if (self.first) |mr| {
- try js_this.set("0", mr);
- }
- }
-};
-
pub const MutationRecord = struct {
type: []const u8,
target: *parser.Node,
- addedNodes: NodeList = NodeList.init(),
- removedNodes: NodeList = NodeList.init(),
- previousSibling: ?*parser.Node = null,
- nextSibling: ?*parser.Node = null,
- attributeName: ?[]const u8 = null,
- attributeNamespace: ?[]const u8 = null,
- oldValue: ?[]const u8 = null,
+ added_nodes: NodeList = .{},
+ removed_nodes: NodeList = .{},
+ previous_sibling: ?*parser.Node = null,
+ next_sibling: ?*parser.Node = null,
+ attribute_name: ?[]const u8 = null,
+ attribute_namespace: ?[]const u8 = null,
+ old_value: ?[]const u8 = null,
pub fn get_type(self: *const MutationRecord) []const u8 {
return self.type;
}
- pub fn get_addedNodes(self: *const MutationRecord) NodeList {
- return self.addedNodes;
+ pub fn get_addedNodes(self: *MutationRecord) *NodeList {
+ return &self.added_nodes;
}
- pub fn get_removedNodes(self: *const MutationRecord) NodeList {
- return self.addedNodes;
+ pub fn get_removedNodes(self: *MutationRecord) *NodeList {
+ return &self.removed_nodes;
}
pub fn get_target(self: *const MutationRecord) *parser.Node {
@@ -203,136 +171,172 @@ pub const MutationRecord = struct {
}
pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
- return self.attributeName;
+ return self.attribute_name;
}
pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
- return self.attributeNamespace;
+ return self.attribute_namespace;
}
pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
- return self.previousSibling;
+ return self.previous_sibling;
}
pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
- return self.nextSibling;
+ return self.next_sibling;
}
pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
- return self.oldValue;
+ return self.old_value;
}
};
-// EventHandler dedicated to mutation events.
-const EventHandler = struct {
- fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool {
+const MutationObserverInit = struct {
+ childList: bool = false,
+ attributes: bool = false,
+ characterData: bool = false,
+ subtree: bool = false,
+ attributeOldValue: bool = false,
+ characterDataOldValue: bool = false,
+ // TODO
+ // attributeFilter: [][]const u8,
+
+ fn attr(self: MutationObserverInit) bool {
+ return self.attributes or self.attributeOldValue;
+ }
+
+ fn cdata(self: MutationObserverInit) bool {
+ return self.characterData or self.characterDataOldValue;
+ }
+};
+
+const Observer = struct {
+ node: *parser.Node,
+ options: MutationObserverInit,
+
+ // record of the mutation, all observed changes in 1 call are batched
+ record: ?MutationRecord = null,
+
+ // reference back to the MutationObserver so that we can access the arena
+ // and batch the mutation records.
+ mutation_observer: *MutationObserver,
+
+ fn appliesTo(o: *const Observer, target: *parser.Node) bool {
// mutation on any target is always ok.
- if (o.options.subtree) return true;
+ if (o.options.subtree) {
+ return true;
+ }
+
// if target equals node, alway ok.
- if (target == o.node) return true;
+ if (target == o.node) {
+ return true;
+ }
// no subtree, no same target and no childlist, always noky.
- if (!o.options.childList) return false;
+ if (!o.options.childList) {
+ return false;
+ }
// target must be a child of o.node
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = walker.get_next(o.node, next) catch break orelse break;
- if (next.? == target) return true;
+ if (next.? == target) {
+ return true;
+ }
}
return false;
}
- fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
- if (evt == null) return;
-
- var mrs: MutationRecords = .{};
-
- const t = parser.eventType(evt.?) catch |e| {
- log.err("mutation observer event type: {any}", .{e});
- return;
- };
- const et = parser.eventTarget(evt.?) catch |e| {
- log.err("mutation observer event target: {any}", .{e});
- return;
- } orelse return;
- const node = parser.eventTargetToNode(et);
-
+ fn handle(ctx: *anyopaque, event: *parser.Event) void {
// retrieve the observer from the data.
- const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));
+ var self: *Observer = @alignCast(@ptrCast(ctx));
+ var mutation_observer = self.mutation_observer;
- if (!apply(o, node)) return;
+ const node = blk: {
+ const event_target = parser.eventTarget(event) catch |e| {
+ log.err("mutation observer event target: {any}", .{e});
+ return;
+ } orelse return;
- const muevt = parser.eventToMutationEvent(evt.?);
+ break :blk parser.eventTargetToNode(event_target);
+ };
- // TODO get the allocator by another way?
- const alloc = data.cbk.executor.scope_arena;
-
- if (std.mem.eql(u8, t, "DOMAttrModified")) {
- mrs.first = .{
- .type = "attributes",
- .target = o.node,
- .attributeName = parser.mutationEventAttributeName(muevt) catch null,
- };
-
- // record old value if required.
- if (o.options.attributeOldValue) {
- mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
- }
- } else if (std.mem.eql(u8, t, "DOMCharacterDataModified")) {
- mrs.first = .{
- .type = "characterData",
- .target = o.node,
- };
-
- // record old value if required.
- if (o.options.characterDataOldValue) {
- mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
- }
- } else if (std.mem.eql(u8, t, "DOMNodeInserted")) {
- mrs.first = .{
- .type = "childList",
- .target = o.node,
- .addedNodes = NodeList.init(),
- .removedNodes = NodeList.init(),
- };
-
- const rn = parser.mutationEventRelatedNode(muevt) catch null;
- if (rn) |n| {
- mrs.first.?.addedNodes.append(alloc, n) catch |e| {
- log.err("mutation event handler error: {any}", .{e});
- return;
- };
- }
- } else if (std.mem.eql(u8, t, "DOMNodeRemoved")) {
- mrs.first = .{
- .type = "childList",
- .target = o.node,
- .addedNodes = NodeList.init(),
- .removedNodes = NodeList.init(),
- };
-
- const rn = parser.mutationEventRelatedNode(muevt) catch null;
- if (rn) |n| {
- mrs.first.?.removedNodes.append(alloc, n) catch |e| {
- log.err("mutation event handler error: {any}", .{e});
- return;
- };
- }
- } else {
+ if (self.appliesTo(node) == false) {
return;
}
- // TODO pass MutationRecords and MutationObserver
- var result: Env.Callback.Result = undefined;
- data.cbk.tryCall(.{mrs}, &result) catch {
- log.err("mutation observer callback error: {s}", .{result.exception});
- log.debug("stack:\n{s}", .{result.stack orelse "???"});
+ const event_type = blk: {
+ const t = parser.eventType(event) catch |e| {
+ log.err("mutation observer event type: {any}", .{e});
+ return;
+ };
+ break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
+ };
+
+ const arena = mutation_observer.arena;
+ if (self.record == null) {
+ self.record = .{
+ .target = self.node,
+ .type = event_type.recordType(),
+ };
+ mutation_observer.observed.append(arena, &self.record.?) catch |err| {
+ log.err("mutation_observer append: {}", .{err});
+ };
+ }
+
+ var record = &self.record.?;
+ const mutation_event = parser.eventToMutationEvent(event);
+
+ switch (event_type) {
+ .DOMAttrModified => {
+ record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
+ if (self.options.attributeOldValue) {
+ record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
+ }
+ },
+ .DOMCharacterDataModified => {
+ if (self.options.characterDataOldValue) {
+ record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
+ }
+ },
+ .DOMNodeInserted => {
+ if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
+ record.added_nodes.append(arena, related_node) catch |e| {
+ log.err("mutation event handler error: {any}", .{e});
+ return;
+ };
+ }
+ },
+ .DOMNodeRemoved => {
+ if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
+ record.removed_nodes.append(arena, related_node) catch |e| {
+ log.err("mutation event handler error: {any}", .{e});
+ return;
+ };
+ }
+ },
+ }
+ }
+};
+
+const MutationEventType = enum {
+ DOMAttrModified,
+ DOMCharacterDataModified,
+ DOMNodeInserted,
+ DOMNodeRemoved,
+
+ fn recordType(self: MutationEventType) []const u8 {
+ return switch (self) {
+ .DOMAttrModified => "attributes",
+ .DOMCharacterDataModified => "characterData",
+ .DOMNodeInserted => "childList",
+ .DOMNodeRemoved => "childList",
};
}
-}.handle;
+};
const testing = @import("../../testing.zig");
test "Browser.DOM.MutationObserver" {
@@ -384,4 +388,19 @@ test "Browser.DOM.MutationObserver" {
.{ "mrs2[0].target.data", "foo" },
.{ "mrs2[0].oldValue", " And" },
}, .{});
+
+ // tests that mutation observers that have a callback which trigger the
+ // mutation observer don't crash.
+ // https://github.com/lightpanda-io/browser/issues/550
+ try runner.testCases(&.{
+ .{
+ \\ var node = document.getElementById("para");
+ \\ new MutationObserver(() => {
+ \\ node.innerText = 'a';
+ \\ }).observe(document, { subtree:true,childList:true });
+ \\ node.innerText = "2";
+ ,
+ "2",
+ },
+ }, .{});
}
diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig
index 1e1aba1b..22f41dce 100644
--- a/src/browser/dom/node.zig
+++ b/src/browser/dom/node.zig
@@ -275,8 +275,7 @@ pub const Node = struct {
pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
const allocator = state.arena;
- var list = NodeList.init();
- errdefer list.deinit(allocator);
+ var list: NodeList = .{};
var n = try parser.nodeFirstChild(self) orelse return list;
while (true) {
diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig
index 37ef3b31..edb0d1f0 100644
--- a/src/browser/dom/nodelist.zig
+++ b/src/browser/dom/nodelist.zig
@@ -99,16 +99,9 @@ pub const NodeListEntriesIterator = struct {
// see https://dom.spec.whatwg.org/#old-style-collections
pub const NodeList = struct {
pub const Exception = DOMException;
-
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
- nodes: NodesArrayList,
-
- pub fn init() NodeList {
- return NodeList{
- .nodes = NodesArrayList{},
- };
- }
+ nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
// TODO unref all nodes
diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig
index f708dc1c..b7437c5e 100644
--- a/src/browser/html/document.zig
+++ b/src/browser/html/document.zig
@@ -103,8 +103,7 @@ pub const HTMLDocument = struct {
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
const arena = state.arena;
- var list = NodeList.init();
- errdefer list.deinit(arena);
+ var list: NodeList = .{};
if (name.len == 0) return list;
diff --git a/src/events/event.zig b/src/events/event.zig
deleted file mode 100644
index e37d0512..00000000
--- a/src/events/event.zig
+++ /dev/null
@@ -1,263 +0,0 @@
-// Copyright (C) 2023-2024 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 std = @import("std");
-
-const generate = @import("../generate.zig");
-
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const CallbackResult = jsruntime.CallbackResult;
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
-
-const DOMException = @import("../dom/exceptions.zig").DOMException;
-const EventTarget = @import("../dom/event_target.zig").EventTarget;
-const EventTargetUnion = @import("../dom/event_target.zig").Union;
-
-const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
-
-const log = std.log.scoped(.events);
-
-// Event interfaces
-pub const Interfaces = .{
- Event,
- ProgressEvent,
-};
-
-pub const Union = generate.Union(Interfaces);
-
-// https://dom.spec.whatwg.org/#event
-pub const Event = struct {
- pub const Self = parser.Event;
- pub const Exception = DOMException;
- pub const mem_guarantied = true;
-
- pub const EventInit = parser.EventInit;
-
- // JS
- // --
-
- pub const _CAPTURING_PHASE = 1;
- pub const _AT_TARGET = 2;
- pub const _BUBBLING_PHASE = 3;
-
- pub fn toInterface(evt: *parser.Event) !Union {
- return switch (try parser.eventGetInternalType(evt)) {
- .event => .{ .Event = evt },
- .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
- };
- }
-
- pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
- const event = try parser.eventCreate();
- try parser.eventInit(event, eventType, opts orelse EventInit{});
- return event;
- }
-
- // Getters
-
- pub fn get_type(self: *parser.Event) ![]const u8 {
- return try parser.eventType(self);
- }
-
- pub fn get_target(self: *parser.Event) !?EventTargetUnion {
- const et = try parser.eventTarget(self);
- if (et == null) return null;
- return try EventTarget.toInterface(et.?);
- }
-
- pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
- const et = try parser.eventCurrentTarget(self);
- if (et == null) return null;
- return try EventTarget.toInterface(et.?);
- }
-
- pub fn get_eventPhase(self: *parser.Event) !u8 {
- return try parser.eventPhase(self);
- }
-
- pub fn get_bubbles(self: *parser.Event) !bool {
- return try parser.eventBubbles(self);
- }
-
- pub fn get_cancelable(self: *parser.Event) !bool {
- return try parser.eventCancelable(self);
- }
-
- pub fn get_defaultPrevented(self: *parser.Event) !bool {
- return try parser.eventDefaultPrevented(self);
- }
-
- pub fn get_isTrusted(self: *parser.Event) !bool {
- return try parser.eventIsTrusted(self);
- }
-
- pub fn get_timestamp(self: *parser.Event) !u32 {
- return try parser.eventTimestamp(self);
- }
-
- // Methods
-
- pub fn _initEvent(
- self: *parser.Event,
- eventType: []const u8,
- bubbles: ?bool,
- cancelable: ?bool,
- ) !void {
- const opts = EventInit{
- .bubbles = bubbles orelse false,
- .cancelable = cancelable orelse false,
- };
- return try parser.eventInit(self, eventType, opts);
- }
-
- pub fn _stopPropagation(self: *parser.Event) !void {
- return try parser.eventStopPropagation(self);
- }
-
- pub fn _stopImmediatePropagation(self: *parser.Event) !void {
- return try parser.eventStopImmediatePropagation(self);
- }
-
- pub fn _preventDefault(self: *parser.Event) !void {
- return try parser.eventPreventDefault(self);
- }
-};
-
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var common = [_]Case{
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
- .{ .src = "var nb = 0; var evt", .ex = "undefined" },
- };
- try checkCases(js_env, &common);
-
- var basic = [_]Case{
- .{ .src =
- \\content.addEventListener('target',
- \\function(e) {
- \\evt = e; nb = nb + 1;
- \\e.preventDefault();
- \\})
- , .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt.target === content", .ex = "true" },
- .{ .src = "evt.bubbles", .ex = "true" },
- .{ .src = "evt.cancelable", .ex = "true" },
- .{ .src = "evt.defaultPrevented", .ex = "true" },
- .{ .src = "evt.isTrusted", .ex = "true" },
- .{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
- // event.type, event.currentTarget, event.phase checked in EventTarget
- };
- try checkCases(js_env, &basic);
-
- var stop = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('stop',
- \\function(e) {
- \\e.stopPropagation();
- \\nb = nb + 1;
- \\}, true)
- , .ex = "undefined" },
- // the following event listener will not be invoked
- .{ .src =
- \\para.addEventListener('stop',
- \\function(e) {
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
- };
- try checkCases(js_env, &stop);
-
- var stop_immediate = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('immediate',
- \\function(e) {
- \\e.stopImmediatePropagation();
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- // the following event listener will not be invoked
- .{ .src =
- \\content.addEventListener('immediate',
- \\function(e) {
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
- };
- try checkCases(js_env, &stop_immediate);
-
- var legacy = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('legacy',
- \\function(e) {
- \\evt = e; nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
- .{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- };
- try checkCases(js_env, &legacy);
-
- var remove = [_]Case{
- .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
- .{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
- .{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
- .{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
- .{ .src = "nb", .ex = "0" },
- };
- try checkCases(js_env, &remove);
-}
-
-pub const EventHandler = struct {
- fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
- // TODO get the allocator by another way?
- var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
- defer res.deinit();
-
- if (event) |evt| {
- data.cbk.trycall(.{
- Event.toInterface(evt) catch unreachable,
- }, &res) catch |e| log.err("event handler error: {any}", .{e});
- } else {
- data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
- }
-
- // in case of function error, we log the result and the trace.
- if (!res.success) {
- log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
- log.debug("{s}", .{res.stack orelse "no stack trace"});
- }
- }
-}.handle;
diff --git a/src/runtime/js.zig b/src/runtime/js.zig
index f10dcecd..b861aa91 100644
--- a/src/runtime/js.zig
+++ b/src/runtime/js.zig
@@ -664,11 +664,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
- .void, .bool, .int, .comptime_int, .float, .comptime_float, .array => {
+ .void, .bool, .int, .comptime_int, .float, .comptime_float => {
// Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases.
unreachable;
},
+ .array => {
+ var js_arr = v8.Array.init(isolate, value.len);
+ var js_obj = js_arr.castTo(v8.Object);
+ for (value, 0..) |v, i| {
+ const js_val = try zigValueToJs(templates, isolate, context, v);
+ if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
+ return error.FailedToCreateArray;
+ }
+ }
+ return js_obj.toValue();
+ },
.pointer => |ptr| switch (ptr.size) {
.one => {
const type_name = @typeName(ptr.child);
@@ -1021,6 +1032,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return gop.value_ptr.*;
}
+ if (comptime @hasDecl(ptr.child, "jsScopeEnd")) {
+ try scope.scope_end_callbacks.append(scope_arena, ScopeEndCallback.init(value));
+ }
+
// Sometimes we're creating a new v8.Object, like when
// we're returning a value from a function. In those cases
// we have the FunctionTemplate, and we can get an object
@@ -1134,6 +1149,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Scope = struct {
arena: Allocator,
handle_scope: v8.HandleScope,
+ scope_end_callbacks: std.ArrayListUnmanaged(ScopeEndCallback) = .{},
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1153,6 +1169,34 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
};
+ // An interface for types that want to their jsScopeEnd function to be
+ // called when the scope ends
+ const ScopeEndCallback = struct {
+ ptr: *anyopaque,
+ scopeEndFn: *const fn (ptr: *anyopaque, executor: *Executor) void,
+
+ fn init(ptr: anytype) ScopeEndCallback {
+ const T = @TypeOf(ptr);
+ const ptr_info = @typeInfo(T);
+
+ const gen = struct {
+ pub fn scopeEnd(pointer: *anyopaque, executor: *Executor) void {
+ const self: T = @ptrCast(@alignCast(pointer));
+ return ptr_info.pointer.child.jsScopeEnd(self, executor);
+ }
+ };
+
+ return .{
+ .ptr = ptr,
+ .scopeEndFn = gen.scopeEnd,
+ };
+ }
+
+ pub fn scopeEnd(self: ScopeEndCallback, executor: *Executor) void {
+ self.scopeEndFn(self.ptr, executor);
+ }
+ };
+
pub const Callback = struct {
id: usize,
executor: *Executor,
@@ -1575,7 +1619,6 @@ fn Caller(comptime E: type) type {
fn deinit(self: *Self) void {
const executor = self.executor;
const call_depth = executor.call_depth - 1;
- executor.call_depth = call_depth;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
@@ -1588,9 +1631,20 @@ fn Caller(comptime E: type) type {
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
+
if (call_depth == 0) {
+ const scope = &self.executor.scope.?;
+ for (scope.scope_end_callbacks.items) |cb| {
+ cb.scopeEnd(executor);
+ }
+
_ = executor._call_arena_instance.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
+
+ // Set this _after_ we've executed the above code, so that if the
+ // above code executes any callbacks, they aren't being executed
+ // at scope 0, which would be wrong.
+ executor.call_depth = call_depth;
}
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {