Skip to content

Commit 94e47f4

Browse files
committed
mutation: implement attribute and cdata observer
1 parent 12111d4 commit 94e47f4

File tree

2 files changed

+329
-6
lines changed

2 files changed

+329
-6
lines changed

src/dom/mutation_observer.zig

Lines changed: 304 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const parser = @import("netsurf");
2222

2323
const jsruntime = @import("jsruntime");
2424
const Callback = jsruntime.Callback;
25+
const CallbackResult = jsruntime.CallbackResult;
2526
const Case = jsruntime.test_utils.Case;
2627
const checkCases = jsruntime.test_utils.checkCases;
2728

@@ -31,14 +32,35 @@ const NodeList = @import("nodelist.zig").NodeList;
3132

3233
pub const Interfaces = generate.Tuple(.{
3334
MutationObserver,
35+
MutationRecord,
36+
MutationRecords,
3437
});
3538

39+
const Walker = @import("../dom/walker.zig").WalkerChildren;
40+
41+
const log = std.log.scoped(.events);
42+
3643
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
3744
pub const MutationObserver = struct {
3845
cbk: Callback,
46+
observers: Observers,
3947

4048
pub const mem_guarantied = true;
4149

50+
const Observer = struct {
51+
node: *parser.Node,
52+
options: MutationObserverInit,
53+
};
54+
55+
const deinitFunc = struct {
56+
fn deinit(ctx: ?*anyopaque, alloc: std.mem.Allocator) void {
57+
const o: *Observer = @ptrCast(@alignCast(ctx));
58+
alloc.destroy(o);
59+
}
60+
}.deinit;
61+
62+
const Observers = std.ArrayListUnmanaged(*Observer);
63+
4264
pub const MutationObserverInit = struct {
4365
childList: bool = false,
4466
attributes: bool = false,
@@ -48,24 +70,260 @@ pub const MutationObserver = struct {
4870
characterDataOldValue: bool = false,
4971
// TODO
5072
// attributeFilter: [][]const u8,
73+
74+
fn attr(self: MutationObserverInit) bool {
75+
return self.attributes or self.attributeOldValue;
76+
}
77+
78+
fn cdata(self: MutationObserverInit) bool {
79+
return self.characterData or self.characterDataOldValue;
80+
}
5181
};
5282

5383
pub fn constructor(cbk: Callback) !MutationObserver {
5484
return MutationObserver{
5585
.cbk = cbk,
86+
.observers = .{},
5687
};
5788
}
5889

59-
pub fn _observe(
60-
_: *MutationObserver,
61-
_: *parser.Node,
62-
_: ?MutationObserverInit,
63-
) !void {}
90+
// TODO
91+
fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit {
92+
return opt orelse .{};
93+
}
94+
95+
pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void {
96+
const o = try alloc.create(Observer);
97+
o.* = .{
98+
.node = node,
99+
.options = resolveOptions(options),
100+
};
101+
errdefer alloc.destroy(o);
102+
103+
// register the new observer.
104+
try self.observers.append(alloc, o);
105+
106+
// register node's events.
107+
if (o.options.childList or o.options.subtree) {
108+
try parser.eventTargetAddEventListener(
109+
parser.toEventTarget(parser.Node, node),
110+
alloc,
111+
"DOMNodeInserted",
112+
EventHandler,
113+
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
114+
false,
115+
);
116+
try parser.eventTargetAddEventListener(
117+
parser.toEventTarget(parser.Node, node),
118+
alloc,
119+
"DOMNodeRemoved",
120+
EventHandler,
121+
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
122+
false,
123+
);
124+
}
125+
if (o.options.attr()) {
126+
try parser.eventTargetAddEventListener(
127+
parser.toEventTarget(parser.Node, node),
128+
alloc,
129+
"DOMAttrModified",
130+
EventHandler,
131+
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
132+
false,
133+
);
134+
}
135+
if (o.options.cdata()) {
136+
try parser.eventTargetAddEventListener(
137+
parser.toEventTarget(parser.Node, node),
138+
alloc,
139+
"DOMCharacterDataModified",
140+
EventHandler,
141+
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
142+
false,
143+
);
144+
}
145+
if (o.options.subtree) {
146+
try parser.eventTargetAddEventListener(
147+
parser.toEventTarget(parser.Node, node),
148+
alloc,
149+
"DOMSubtreeModified",
150+
EventHandler,
151+
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
152+
false,
153+
);
154+
}
155+
}
156+
157+
// TODO
158+
pub fn _disconnect(_: *MutationObserver) !void {
159+
// TODO unregister listeners.
160+
}
161+
162+
pub fn deinit(self: *MutationObserver, alloc: std.mem.Allocator) void {
163+
// TODO unregister listeners.
164+
for (self.observers.items) |o| alloc.destroy(o);
165+
self.observers.deinit(alloc);
166+
}
64167

65168
// TODO
66-
pub fn _disconnect(_: *MutationObserver) !void {}
169+
pub fn _takeRecords(_: MutationObserver) ?[]const u8 {
170+
return &[_]u8{};
171+
}
67172
};
68173

174+
// Handle multiple record?
175+
pub const MutationRecords = struct {
176+
first: ?MutationRecord = null,
177+
178+
pub const mem_guarantied = true;
179+
180+
pub fn get_length(self: *MutationRecords) u32 {
181+
if (self.first == null) return 0;
182+
183+
return 1;
184+
}
185+
186+
pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void {
187+
if (self.first) |mr| {
188+
try js_obj.set("0", mr);
189+
}
190+
}
191+
};
192+
193+
pub const MutationRecord = struct {
194+
type: []const u8,
195+
target: *parser.Node,
196+
addedNodes: NodeList = NodeList.init(),
197+
removedNodes: NodeList = NodeList.init(),
198+
previousSibling: ?*parser.Node = null,
199+
nextSibling: ?*parser.Node = null,
200+
attributeName: ?[]const u8 = null,
201+
attributeNamespace: ?[]const u8 = null,
202+
oldValue: ?[]const u8 = null,
203+
204+
pub const mem_guarantied = true;
205+
206+
pub fn get_type(self: MutationRecord) []const u8 {
207+
return self.type;
208+
}
209+
210+
pub fn get_addedNodes(self: MutationRecord) NodeList {
211+
return self.addedNodes;
212+
}
213+
214+
pub fn get_removedNodes(self: MutationRecord) NodeList {
215+
return self.addedNodes;
216+
}
217+
218+
pub fn get_target(self: MutationRecord) *parser.Node {
219+
return self.target;
220+
}
221+
222+
pub fn get_attributeName(self: MutationRecord) ?[]const u8 {
223+
return self.attributeName;
224+
}
225+
226+
pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 {
227+
return self.attributeNamespace;
228+
}
229+
230+
pub fn get_previousSibling(self: MutationRecord) ?*parser.Node {
231+
return self.previousSibling;
232+
}
233+
234+
pub fn get_nextSibling(self: MutationRecord) ?*parser.Node {
235+
return self.nextSibling;
236+
}
237+
238+
pub fn get_oldValue(self: MutationRecord) ?[]const u8 {
239+
return self.oldValue;
240+
}
241+
};
242+
243+
// EventHandler dedicated to mutation events.
244+
const EventHandler = struct {
245+
fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool {
246+
// mutation on any target is always ok.
247+
if (o.options.subtree) return true;
248+
// if target equals node, alway ok.
249+
if (target == o.node) return true;
250+
251+
// no subtree, no same target and no childlist, always noky.
252+
if (!o.options.childList) return false;
253+
254+
// target must be a child of o.node
255+
const walker = Walker{};
256+
var next: ?*parser.Node = null;
257+
while (true) {
258+
next = walker.get_next(o.node, next) catch break orelse break;
259+
if (next.? == target) return true;
260+
}
261+
262+
return false;
263+
}
264+
265+
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
266+
if (evt == null) return;
267+
268+
var mrs: MutationRecords = .{};
269+
270+
const t = parser.eventType(evt.?) catch |e| {
271+
log.err("mutation observer event type: {any}", .{e});
272+
return;
273+
};
274+
const et = parser.eventTarget(evt.?) catch |e| {
275+
log.err("mutation observer event target: {any}", .{e});
276+
return;
277+
} orelse return;
278+
const node = parser.eventTargetToNode(et);
279+
280+
// retrieve the observer from the data.
281+
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
282+
283+
if (!apply(o, node)) return;
284+
285+
const muevt = parser.eventToMutationEvent(evt.?);
286+
287+
if (std.mem.eql(u8, t, "DOMAttrModified")) {
288+
mrs.first = .{
289+
.type = "attributes",
290+
.target = o.node,
291+
.attributeName = parser.mutationEventAttributeName(muevt) catch null,
292+
};
293+
294+
// record old value if required.
295+
if (o.options.attributeOldValue) {
296+
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
297+
}
298+
} else if (std.mem.eql(u8, t, "DOMCharacterDataModified")) {
299+
mrs.first = .{
300+
.type = "characterData",
301+
.target = o.node,
302+
};
303+
304+
// record old value if required.
305+
if (o.options.characterDataOldValue) {
306+
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
307+
}
308+
} else {
309+
return;
310+
}
311+
312+
// TODO get the allocator by another way?
313+
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
314+
defer res.deinit();
315+
316+
// TODO pass MutationRecords and MutationObserver
317+
data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e});
318+
319+
// in case of function error, we log the result and the trace.
320+
if (!res.success) {
321+
log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"});
322+
log.debug("{s}", .{res.stack orelse "no stack trace"});
323+
}
324+
}
325+
}.handle;
326+
69327
pub fn testExecFn(
70328
_: std.mem.Allocator,
71329
js_env: *jsruntime.Env,
@@ -74,4 +332,44 @@ pub fn testExecFn(
74332
.{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" },
75333
};
76334
try checkCases(js_env, &constructor);
335+
336+
var attr = [_]Case{
337+
.{ .src =
338+
\\var nb = 0;
339+
\\var mrs;
340+
\\new MutationObserver((mu) => {
341+
\\ mrs = mu;
342+
\\ nb++;
343+
\\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
344+
\\document.firstElementChild.setAttribute("foo", "bar");
345+
\\// ignored b/c it's about another target.
346+
\\document.firstElementChild.firstChild.setAttribute("foo", "bar");
347+
\\nb;
348+
, .ex = "1" },
349+
.{ .src = "mrs[0].type", .ex = "attributes" },
350+
.{ .src = "mrs[0].target == document.firstElementChild", .ex = "true" },
351+
.{ .src = "mrs[0].target.getAttribute('foo')", .ex = "bar" },
352+
.{ .src = "mrs[0].attributeName", .ex = "foo" },
353+
.{ .src = "mrs[0].oldValue", .ex = "null" },
354+
};
355+
try checkCases(js_env, &attr);
356+
357+
var cdata = [_]Case{
358+
.{ .src =
359+
\\var node = document.getElementById("para").firstChild;
360+
\\var nb2 = 0;
361+
\\var mrs2;
362+
\\new MutationObserver((mu) => {
363+
\\ mrs2 = mu;
364+
\\ nb2++;
365+
\\}).observe(node, { characterData: true, characterDataOldValue: true });
366+
\\node.data = "foo";
367+
\\nb2;
368+
, .ex = "1" },
369+
.{ .src = "mrs2[0].type", .ex = "characterData" },
370+
.{ .src = "mrs2[0].target == node", .ex = "true" },
371+
.{ .src = "mrs2[0].target.data", .ex = "foo" },
372+
.{ .src = "mrs2[0].oldValue", .ex = " And" },
373+
};
374+
try checkCases(js_env, &cdata);
77375
}

src/netsurf/netsurf.zig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,27 @@ pub const EventType = enum(u8) {
522522
progress_event = 1,
523523
};
524524

525+
pub const MutationEvent = c.dom_mutation_event;
526+
527+
pub fn eventToMutationEvent(evt: *Event) *MutationEvent {
528+
return @as(*MutationEvent, @ptrCast(evt));
529+
}
530+
531+
pub fn mutationEventAttributeName(evt: *MutationEvent) ![]const u8 {
532+
var s: ?*String = undefined;
533+
const err = c._dom_mutation_event_get_attr_name(evt, &s);
534+
try DOMErr(err);
535+
return strToData(s.?);
536+
}
537+
538+
pub fn mutationEventPrevValue(evt: *MutationEvent) !?[]const u8 {
539+
var s: ?*String = undefined;
540+
const err = c._dom_mutation_event_get_prev_value(evt, &s);
541+
try DOMErr(err);
542+
if (s == null) return null;
543+
return strToData(s.?);
544+
}
545+
525546
// EventListener
526547
pub const EventListener = c.dom_event_listener;
527548
const EventListenerEntry = c.listener_entry;
@@ -533,6 +554,10 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
533554
// EventTarget
534555
pub const EventTarget = c.dom_event_target;
535556

557+
pub fn eventTargetToNode(et: *EventTarget) *Node {
558+
return @as(*Node, @ptrCast(et));
559+
}
560+
536561
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
537562
// retrieve the vtable
538563
const vtable = et.*.vtable.?;

0 commit comments

Comments
 (0)