mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Improve MutationObserver
- Fix get_removedNodes (it was returning addedNodes)
- get_removedNodes and get addedNodes now return references
- used enum for dispatching and clean up dispatching in general
- Remove MutationRecords and simply return an array
- this allows the returned records to be iterable (as they should be)
- jsruntime ZigToJs will now map a Zig array to a JS array
-Rely on default initialize of NodeList
-Batch observed records
- Callback only executed when call_depth == 0
- Fixes crashes when a MutationObserver callback mutated the nodes being
observes.
- Fixes some WPT issues, but Netsurf's mutationEventRelatedNode does not
appear to be to spec, so most tests fail.
- Allow zig methods to execute arbitrary code when call_depth == 0
- This is a preview of how I hope to make XHR not crash if the CDP session
ends while there's still network activity
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,18 +17,17 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 record.s
|
||||
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",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const 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;
|
||||
@@ -661,11 +661,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);
|
||||
@@ -1018,6 +1029,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
|
||||
@@ -1131,6 +1146,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) = .{},
|
||||
|
||||
@@ -1150,6 +1166,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,
|
||||
@@ -1572,7 +1616,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
|
||||
@@ -1585,9 +1628,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 {
|
||||
|
||||
Reference in New Issue
Block a user