@salve-software/react-native-nitro-jsdom 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/NitroJsdom.podspec +63 -0
  3. package/README.md +95 -0
  4. package/android/CMakeLists.txt +86 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/nitrojsdom/NitroJsdomPackage.kt +20 -0
  11. package/cpp/HybridHtmlSandbox.cpp +171 -0
  12. package/cpp/HybridHtmlSandbox.hpp +47 -0
  13. package/cpp/lexbor/LexborDocument.cpp +365 -0
  14. package/cpp/lexbor/LexborDocument.hpp +71 -0
  15. package/cpp/quickjs/DOMBindings.cpp +57 -0
  16. package/cpp/quickjs/DOMBindings.hpp +21 -0
  17. package/cpp/quickjs/DOMBindingsInternal.cpp +157 -0
  18. package/cpp/quickjs/DOMBindingsInternal.hpp +83 -0
  19. package/cpp/quickjs/MutationObservers.cpp +546 -0
  20. package/cpp/quickjs/MutationObservers.hpp +127 -0
  21. package/cpp/quickjs/QuickJSRuntime.cpp +475 -0
  22. package/cpp/quickjs/QuickJSRuntime.hpp +110 -0
  23. package/cpp/quickjs/Storage.cpp +130 -0
  24. package/cpp/quickjs/Storage.hpp +40 -0
  25. package/cpp/quickjs/bindings/ClassListBindings.cpp +241 -0
  26. package/cpp/quickjs/bindings/ClassListBindings.hpp +29 -0
  27. package/cpp/quickjs/bindings/DatasetBindings.cpp +120 -0
  28. package/cpp/quickjs/bindings/DatasetBindings.hpp +20 -0
  29. package/cpp/quickjs/bindings/DocumentBindings.cpp +155 -0
  30. package/cpp/quickjs/bindings/DocumentBindings.hpp +19 -0
  31. package/cpp/quickjs/bindings/ElementBindings.cpp +1106 -0
  32. package/cpp/quickjs/bindings/ElementBindings.hpp +19 -0
  33. package/cpp/quickjs/bindings/EventBindings.cpp +352 -0
  34. package/cpp/quickjs/bindings/EventBindings.hpp +21 -0
  35. package/cpp/quickjs/bindings/FetchBindings.cpp +347 -0
  36. package/cpp/quickjs/bindings/FetchBindings.hpp +16 -0
  37. package/cpp/quickjs/bindings/StyleBindings.cpp +245 -0
  38. package/cpp/quickjs/bindings/StyleBindings.hpp +22 -0
  39. package/cpp/quickjs/bindings/TimerBindings.cpp +94 -0
  40. package/cpp/quickjs/bindings/TimerBindings.hpp +16 -0
  41. package/cpp/quickjs/bindings/WindowBindings.cpp +211 -0
  42. package/cpp/quickjs/bindings/WindowBindings.hpp +16 -0
  43. package/ios/Bridge.h +8 -0
  44. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +138 -0
  45. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -0
  46. package/lib/commonjs/classes/JSDOM/index.js +17 -0
  47. package/lib/commonjs/classes/JSDOM/index.js.map +1 -0
  48. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js +2 -0
  49. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  50. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js +6 -0
  51. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  52. package/lib/commonjs/classes/JSDOM/types/index.js +2 -0
  53. package/lib/commonjs/classes/JSDOM/types/index.js.map +1 -0
  54. package/lib/commonjs/classes/index.js +17 -0
  55. package/lib/commonjs/classes/index.js.map +1 -0
  56. package/lib/commonjs/index.js +17 -0
  57. package/lib/commonjs/index.js.map +1 -0
  58. package/lib/commonjs/package.json +1 -0
  59. package/lib/commonjs/specs/HtmlSandbox.nitro.js +6 -0
  60. package/lib/commonjs/specs/HtmlSandbox.nitro.js.map +1 -0
  61. package/lib/module/classes/JSDOM/JSDOM.class.js +133 -0
  62. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -0
  63. package/lib/module/classes/JSDOM/index.js +4 -0
  64. package/lib/module/classes/JSDOM/index.js.map +1 -0
  65. package/lib/module/classes/JSDOM/types/IFetchOptions.js +2 -0
  66. package/lib/module/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  67. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js +4 -0
  68. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  69. package/lib/module/classes/JSDOM/types/index.js +2 -0
  70. package/lib/module/classes/JSDOM/types/index.js.map +1 -0
  71. package/lib/module/classes/index.js +4 -0
  72. package/lib/module/classes/index.js.map +1 -0
  73. package/lib/module/index.js +4 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/specs/HtmlSandbox.nitro.js +4 -0
  76. package/lib/module/specs/HtmlSandbox.nitro.js.map +1 -0
  77. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts +74 -0
  78. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -0
  79. package/lib/typescript/src/classes/JSDOM/index.d.ts +3 -0
  80. package/lib/typescript/src/classes/JSDOM/index.d.ts.map +1 -0
  81. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts +14 -0
  82. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts.map +1 -0
  83. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +57 -0
  84. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -0
  85. package/lib/typescript/src/classes/JSDOM/types/index.d.ts +3 -0
  86. package/lib/typescript/src/classes/JSDOM/types/index.d.ts.map +1 -0
  87. package/lib/typescript/src/classes/index.d.ts +2 -0
  88. package/lib/typescript/src/classes/index.d.ts.map +1 -0
  89. package/lib/typescript/src/index.d.ts +2 -0
  90. package/lib/typescript/src/index.d.ts.map +1 -0
  91. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +13 -0
  92. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -0
  93. package/nitro.json +26 -0
  94. package/nitrogen/generated/.gitattributes +1 -0
  95. package/nitrogen/generated/android/NitroJsdom+autolinking.cmake +81 -0
  96. package/nitrogen/generated/android/NitroJsdom+autolinking.gradle +27 -0
  97. package/nitrogen/generated/android/NitroJsdomOnLoad.cpp +49 -0
  98. package/nitrogen/generated/android/NitroJsdomOnLoad.hpp +34 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrojsdom/NitroJsdomOnLoad.kt +35 -0
  100. package/nitrogen/generated/ios/NitroJsdom+autolinking.rb +62 -0
  101. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.cpp +17 -0
  102. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.hpp +27 -0
  103. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Umbrella.hpp +38 -0
  104. package/nitrogen/generated/ios/NitroJsdomAutolinking.mm +35 -0
  105. package/nitrogen/generated/ios/NitroJsdomAutolinking.swift +16 -0
  106. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.cpp +26 -0
  107. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +73 -0
  108. package/package.json +131 -0
  109. package/src/classes/JSDOM/JSDOM.class.ts +145 -0
  110. package/src/classes/JSDOM/index.ts +2 -0
  111. package/src/classes/JSDOM/types/IFetchOptions.ts +14 -0
  112. package/src/classes/JSDOM/types/IJSDOMOptions.ts +57 -0
  113. package/src/classes/JSDOM/types/index.ts +2 -0
  114. package/src/classes/index.ts +1 -0
  115. package/src/index.ts +1 -0
  116. package/src/specs/HtmlSandbox.nitro.ts +33 -0
@@ -0,0 +1,546 @@
1
+ #include "MutationObservers.hpp"
2
+ #include "DOMBindingsInternal.hpp"
3
+ #include "QuickJSRuntime.hpp"
4
+ #include "quickjs.h"
5
+ #include <lexbor/dom/dom.h>
6
+ #include <algorithm>
7
+
8
+ namespace margelo::nitro::nitrojsdom {
9
+
10
+ // ── Forward declarations ───────────────────────────────────────────────────────
11
+
12
+ struct MutationObserverOpaque;
13
+ struct DispatchJob;
14
+
15
+ // ── JS Class IDs ──────────────────────────────────────────────────────────────
16
+
17
+ static JSClassID js_mutation_observer_class_id = 0;
18
+ static JSClassID js_dispatch_carrier_class_id = 0;
19
+
20
+ static void mo_finalizer(JSRuntime*, JSValue val) {
21
+ auto* opaque = static_cast<MutationObserverOpaque*>(
22
+ JS_GetOpaque(val, js_mutation_observer_class_id));
23
+ delete opaque;
24
+ }
25
+
26
+ static void carrier_finalizer(JSRuntime*, JSValue val) {
27
+ auto* job = static_cast<DispatchJob*>(JS_GetOpaque(val, js_dispatch_carrier_class_id));
28
+ delete job;
29
+ }
30
+
31
+ static JSClassDef js_mutation_observer_class = { "MutationObserver", .finalizer = mo_finalizer };
32
+ static JSClassDef js_dispatch_carrier_class = { "_DispatchCarrier", .finalizer = carrier_finalizer };
33
+
34
+ // ── MutationObserver opaque data ──────────────────────────────────────────────
35
+
36
+ struct MutationObserverOpaque {
37
+ MutationObservers* registry { nullptr };
38
+ uint32_t id { 0 };
39
+ };
40
+
41
+ // ── Dispatch trampoline (passed to JS_EnqueueJob) ─────────────────────────────
42
+
43
+ // The job data: a heap-allocated pair of (registry*, observer_id).
44
+ // We can't store a RegisteredObserver* because the vector may reallocate.
45
+ struct DispatchJob {
46
+ MutationObservers* registry { nullptr };
47
+ uint32_t observer_id { 0 };
48
+ uint32_t generation { 0 };
49
+ };
50
+
51
+ static JSValue dispatch_trampoline(JSContext* ctx, int argc, JSValue* argv) {
52
+ (void)argc; (void)argv;
53
+ // argv[0] holds a pointer to DispatchJob encoded as a carrier opaque object
54
+ DispatchJob* job = static_cast<DispatchJob*>(JS_GetOpaque(argv[0], js_dispatch_carrier_class_id));
55
+ if (!job) return JS_UNDEFINED;
56
+
57
+ MutationObservers* reg = job->registry;
58
+ uint32_t obs_id = job->observer_id;
59
+
60
+ // Find the observer
61
+ RegisteredObserver* obs = nullptr;
62
+ for (auto& o : reg->_observers_ref()) {
63
+ if (o.id == obs_id) { obs = &o; break; }
64
+ }
65
+
66
+ if (!obs || obs->disconnected || obs->queue.empty() ||
67
+ obs->dispatch_generation != job->generation) {
68
+ if (obs) obs->dispatch_scheduled = false;
69
+ return JS_UNDEFINED;
70
+ }
71
+
72
+ // Mark as no longer scheduled BEFORE calling callback (so re-entry works)
73
+ obs->dispatch_scheduled = false;
74
+
75
+ // Drain and materialize the record queue
76
+ auto records = std::move(obs->queue);
77
+ obs->queue.clear();
78
+
79
+ if (!obs->js_callback) return JS_UNDEFINED;
80
+ JSValue* cb = static_cast<JSValue*>(obs->js_callback);
81
+
82
+ // Build JS array of MutationRecord objects
83
+ JSValue arr = JS_NewArray(ctx);
84
+ for (size_t i = 0; i < records.size(); i++) {
85
+ auto& r = records[i];
86
+
87
+ // Skip records whose target node is no longer in the document
88
+ if (!reg->isInDocument_public(r.target)) continue;
89
+
90
+ JSValue rec_obj = JS_NewObject(ctx);
91
+ JS_SetPropertyStr(ctx, rec_obj, "type", JS_NewString(ctx, r.type.c_str()));
92
+ JS_SetPropertyStr(ctx, rec_obj, "target", make_element(ctx, r.target));
93
+
94
+ // addedNodes array
95
+ JSValue added = JS_NewArray(ctx);
96
+ for (size_t j = 0; j < r.addedNodes.size(); j++)
97
+ JS_SetPropertyUint32(ctx, added, (uint32_t)j, make_element(ctx, r.addedNodes[j]));
98
+ JS_SetPropertyStr(ctx, rec_obj, "addedNodes", added);
99
+
100
+ // removedNodes array
101
+ JSValue removed = JS_NewArray(ctx);
102
+ for (size_t j = 0; j < r.removedNodes.size(); j++)
103
+ JS_SetPropertyUint32(ctx, removed, (uint32_t)j, make_element(ctx, r.removedNodes[j]));
104
+ JS_SetPropertyStr(ctx, rec_obj, "removedNodes", removed);
105
+
106
+ JS_SetPropertyStr(ctx, rec_obj, "previousSibling",
107
+ r.previousSibling ? make_element(ctx, r.previousSibling) : JS_NULL);
108
+ JS_SetPropertyStr(ctx, rec_obj, "nextSibling",
109
+ r.nextSibling ? make_element(ctx, r.nextSibling) : JS_NULL);
110
+ JS_SetPropertyStr(ctx, rec_obj, "attributeName",
111
+ r.attributeName.empty() ? JS_NULL : JS_NewString(ctx, r.attributeName.c_str()));
112
+ JS_SetPropertyStr(ctx, rec_obj, "oldValue",
113
+ r.oldValue.has_value() ? JS_NewString(ctx, r.oldValue->c_str()) : JS_NULL);
114
+
115
+ JS_SetPropertyUint32(ctx, arr, (uint32_t)i, rec_obj);
116
+ }
117
+
118
+ // Call the user callback: cb([records])
119
+ JSValue ret = JS_Call(ctx, *cb, JS_UNDEFINED, 1, &arr);
120
+ JS_FreeValue(ctx, arr);
121
+
122
+ if (JS_IsException(ret)) {
123
+ // Route exception as unhandled rejection via RuntimeContext pending_rejection
124
+ RuntimeContext* rctx = static_cast<RuntimeContext*>(JS_GetContextOpaque(ctx));
125
+ if (rctx) {
126
+ if (rctx->pending_rejection) {
127
+ JSValue* old = static_cast<JSValue*>(rctx->pending_rejection);
128
+ JS_FreeValue(ctx, *old);
129
+ delete old;
130
+ rctx->pending_rejection = nullptr;
131
+ }
132
+ JSValue ex = JS_GetException(ctx);
133
+ rctx->pending_rejection = new JSValue(JS_DupValue(ctx, ex));
134
+ JS_FreeValue(ctx, ex);
135
+ }
136
+ JS_FreeValue(ctx, ret);
137
+ return JS_UNDEFINED;
138
+ }
139
+ JS_FreeValue(ctx, ret);
140
+ return JS_UNDEFINED;
141
+ }
142
+
143
+ // ── JS MutationObserver constructor ──────────────────────────────────────────
144
+
145
+ static JSValue js_MutationObserver_constructor(JSContext* ctx, JSValue new_target,
146
+ int argc, JSValue* argv) {
147
+ (void)new_target;
148
+ if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
149
+ JS_ThrowTypeError(ctx, "MutationObserver requires a callback function");
150
+ return JS_EXCEPTION;
151
+ }
152
+
153
+ RuntimeContext* rctx = static_cast<RuntimeContext*>(JS_GetContextOpaque(ctx));
154
+ if (!rctx || !rctx->mutation_observers) return JS_UNDEFINED;
155
+
156
+ // Dup the callback and store in the registry (target set later via observe())
157
+ void* cb = new JSValue(JS_DupValue(ctx, argv[0]));
158
+ uint32_t id = rctx->mutation_observers->addObserver(nullptr, ObserverInit{}, cb);
159
+
160
+ // Create JS object
161
+ JSValue obj = JS_NewObjectClass(ctx, js_mutation_observer_class_id);
162
+ auto* opaque = new MutationObserverOpaque{ rctx->mutation_observers.get(), id };
163
+ JS_SetOpaque(obj, opaque);
164
+
165
+ return obj;
166
+ }
167
+
168
+ // ── JS MutationObserver.observe(target, options) ──────────────────────────────
169
+
170
+ static JSValue js_MutationObserver_observe(JSContext* ctx, JSValue this_val,
171
+ int argc, JSValue* argv) {
172
+ auto* opaque = static_cast<MutationObserverOpaque*>(
173
+ JS_GetOpaque(this_val, js_mutation_observer_class_id));
174
+ if (!opaque || !opaque->registry) return JS_UNDEFINED;
175
+ if (argc < 1) return JS_UNDEFINED;
176
+
177
+ void* target = unwrap_node(ctx, argv[0]);
178
+ if (!target) return JS_UNDEFINED;
179
+
180
+ ObserverInit opts;
181
+ if (argc >= 2 && JS_IsObject(argv[1])) {
182
+ JSValue v;
183
+ auto getBool = [&](const char* name) -> bool {
184
+ v = JS_GetPropertyStr(ctx, argv[1], name);
185
+ bool r = JS_ToBool(ctx, v);
186
+ JS_FreeValue(ctx, v);
187
+ return r;
188
+ };
189
+ opts.childList = getBool("childList");
190
+ opts.attributes = getBool("attributes");
191
+ opts.characterData = getBool("characterData");
192
+ opts.subtree = getBool("subtree");
193
+ opts.attributeOldValue = getBool("attributeOldValue");
194
+ opts.characterDataOldValue = getBool("characterDataOldValue");
195
+
196
+ // attributeFilter
197
+ JSValue filterVal = JS_GetPropertyStr(ctx, argv[1], "attributeFilter");
198
+ if (JS_IsArray(ctx, filterVal)) {
199
+ std::vector<std::string> filters;
200
+ JSValue lenVal = JS_GetPropertyStr(ctx, filterVal, "length");
201
+ int32_t len = 0;
202
+ JS_ToInt32(ctx, &len, lenVal);
203
+ JS_FreeValue(ctx, lenVal);
204
+ for (int32_t i = 0; i < len; i++) {
205
+ JSValue item = JS_GetPropertyUint32(ctx, filterVal, (uint32_t)i);
206
+ const char* s = JS_ToCString(ctx, item);
207
+ if (s) { filters.push_back(s); JS_FreeCString(ctx, s); }
208
+ JS_FreeValue(ctx, item);
209
+ }
210
+ opts.attributeFilter = std::move(filters);
211
+ }
212
+ JS_FreeValue(ctx, filterVal);
213
+ }
214
+
215
+ if ((opts.attributeFilter.has_value() || opts.attributeOldValue) && !opts.attributes)
216
+ opts.attributes = true;
217
+ if (opts.characterDataOldValue && !opts.characterData)
218
+ opts.characterData = true;
219
+
220
+ if (!opts.childList && !opts.attributes && !opts.characterData) {
221
+ JS_ThrowTypeError(ctx,
222
+ "The options object must set at least one of 'attributes', 'characterData', or 'childList' to true.");
223
+ return JS_EXCEPTION;
224
+ }
225
+
226
+ // Update the existing observer with the new target and options
227
+ opaque->registry->updateObserver(opaque->id, reinterpret_cast<lxb_dom_node_t*>(target), opts);
228
+ return JS_UNDEFINED;
229
+ }
230
+
231
+ // ── JS MutationObserver.disconnect() ─────────────────────────────────────────
232
+
233
+ static JSValue js_MutationObserver_disconnect(JSContext* ctx, JSValue this_val,
234
+ int, JSValue*) {
235
+ (void)ctx;
236
+ auto* opaque = static_cast<MutationObserverOpaque*>(
237
+ JS_GetOpaque(this_val, js_mutation_observer_class_id));
238
+ if (!opaque || !opaque->registry) return JS_UNDEFINED;
239
+ opaque->registry->disconnect(opaque->id);
240
+ return JS_UNDEFINED;
241
+ }
242
+
243
+ // ── JS MutationObserver.takeRecords() ────────────────────────────────────────
244
+
245
+ static JSValue js_MutationObserver_takeRecords(JSContext* ctx, JSValue this_val,
246
+ int, JSValue*) {
247
+ auto* opaque = static_cast<MutationObserverOpaque*>(
248
+ JS_GetOpaque(this_val, js_mutation_observer_class_id));
249
+ if (!opaque || !opaque->registry) return JS_NewArray(ctx);
250
+
251
+ auto records = opaque->registry->takeRecords(opaque->id);
252
+ JSValue arr = JS_NewArray(ctx);
253
+ for (size_t i = 0; i < records.size(); i++) {
254
+ auto& r = records[i];
255
+ JSValue rec_obj = JS_NewObject(ctx);
256
+ JS_SetPropertyStr(ctx, rec_obj, "type", JS_NewString(ctx, r.type.c_str()));
257
+ JS_SetPropertyStr(ctx, rec_obj, "target", make_element(ctx, r.target));
258
+
259
+ JSValue added = JS_NewArray(ctx);
260
+ for (size_t j = 0; j < r.addedNodes.size(); j++)
261
+ JS_SetPropertyUint32(ctx, added, (uint32_t)j, make_element(ctx, r.addedNodes[j]));
262
+ JS_SetPropertyStr(ctx, rec_obj, "addedNodes", added);
263
+
264
+ JSValue removed = JS_NewArray(ctx);
265
+ for (size_t j = 0; j < r.removedNodes.size(); j++)
266
+ JS_SetPropertyUint32(ctx, removed, (uint32_t)j, make_element(ctx, r.removedNodes[j]));
267
+ JS_SetPropertyStr(ctx, rec_obj, "removedNodes", removed);
268
+
269
+ JS_SetPropertyStr(ctx, rec_obj, "attributeName",
270
+ r.attributeName.empty() ? JS_NULL : JS_NewString(ctx, r.attributeName.c_str()));
271
+ JS_SetPropertyStr(ctx, rec_obj, "oldValue",
272
+ r.oldValue.has_value() ? JS_NewString(ctx, r.oldValue->c_str()) : JS_NULL);
273
+
274
+ JS_SetPropertyUint32(ctx, arr, (uint32_t)i, rec_obj);
275
+ }
276
+ return arr;
277
+ }
278
+
279
+ // ── MutationObservers::install ────────────────────────────────────────────────
280
+
281
+ void MutationObservers::install(JSContext* ctx, void* doc_root) {
282
+ _doc_root = doc_root;
283
+
284
+ // Register JS classes
285
+ JS_NewClassID(&js_mutation_observer_class_id);
286
+ JS_NewClass(JS_GetRuntime(ctx), js_mutation_observer_class_id, &js_mutation_observer_class);
287
+ JS_NewClassID(&js_dispatch_carrier_class_id);
288
+ JS_NewClass(JS_GetRuntime(ctx), js_dispatch_carrier_class_id, &js_dispatch_carrier_class);
289
+
290
+ // Build prototype
291
+ JSValue proto = JS_NewObject(ctx);
292
+ JS_SetPropertyStr(ctx, proto, "observe", JS_NewCFunction(ctx, js_MutationObserver_observe, "observe", 2));
293
+ JS_SetPropertyStr(ctx, proto, "disconnect", JS_NewCFunction(ctx, js_MutationObserver_disconnect, "disconnect", 0));
294
+ JS_SetPropertyStr(ctx, proto, "takeRecords", JS_NewCFunction(ctx, js_MutationObserver_takeRecords, "takeRecords", 0));
295
+ JS_SetClassProto(ctx, js_mutation_observer_class_id, proto);
296
+
297
+ // Register constructor on global
298
+ JSValue global = JS_GetGlobalObject(ctx);
299
+ JSValue ctor = JS_NewCFunction2(ctx, js_MutationObserver_constructor,
300
+ "MutationObserver", 1, JS_CFUNC_constructor, 0);
301
+ JS_SetPropertyStr(ctx, global, "MutationObserver", ctor);
302
+ JS_FreeValue(ctx, global);
303
+ }
304
+
305
+ // ── MutationObservers::addObserver ────────────────────────────────────────────
306
+
307
+ uint32_t MutationObservers::addObserver(void* target, ObserverInit options, void* js_callback) {
308
+ RegisteredObserver obs;
309
+ obs.id = _next_id++;
310
+ obs.target = target;
311
+ obs.options = std::move(options);
312
+ obs.js_callback = js_callback;
313
+ obs.dispatch_scheduled = false;
314
+ obs.disconnected = false;
315
+ _observers.push_back(std::move(obs));
316
+ _active_count++;
317
+ return _observers.back().id;
318
+ }
319
+
320
+ // ── MutationObservers::updateObserver ────────────────────────────────────────
321
+
322
+ void MutationObservers::updateObserver(uint32_t id, void* target, ObserverInit options) {
323
+ for (auto& obs : _observers) {
324
+ if (obs.id == id) {
325
+ if (obs.disconnected) {
326
+ // Re-activating a previously disconnected observer
327
+ obs.disconnected = false;
328
+ _active_count++;
329
+ }
330
+ obs.target = target;
331
+ obs.options = std::move(options);
332
+ return;
333
+ }
334
+ }
335
+ }
336
+
337
+ // ── MutationObservers::disconnect ────────────────────────────────────────────
338
+
339
+ void MutationObservers::disconnect(uint32_t observer_id) {
340
+ for (auto& obs : _observers) {
341
+ if (obs.id == observer_id) {
342
+ if (!obs.disconnected) {
343
+ obs.disconnected = true;
344
+ if (_active_count > 0) _active_count--;
345
+ }
346
+ obs.queue.clear();
347
+ obs.dispatch_scheduled = false;
348
+ return;
349
+ }
350
+ }
351
+ }
352
+
353
+ // ── MutationObservers::takeRecords ───────────────────────────────────────────
354
+
355
+ std::vector<MutationRecord> MutationObservers::takeRecords(uint32_t observer_id) {
356
+ for (auto& obs : _observers) {
357
+ if (obs.id == observer_id) {
358
+ auto records = std::move(obs.queue);
359
+ obs.queue.clear();
360
+ obs.dispatch_scheduled = false;
361
+ obs.dispatch_generation++;
362
+ return records;
363
+ }
364
+ }
365
+ return {};
366
+ }
367
+
368
+ // ── Ancestry helpers ──────────────────────────────────────────────────────────
369
+
370
+ bool MutationObservers::isInDocument(void* node_ptr) const {
371
+ if (!node_ptr || !_doc_root) return false;
372
+ lxb_dom_node_t* node = static_cast<lxb_dom_node_t*>(node_ptr);
373
+ lxb_dom_node_t* walk = node;
374
+ while (walk) {
375
+ if (walk == static_cast<lxb_dom_node_t*>(_doc_root)) return true;
376
+ walk = walk->parent;
377
+ }
378
+ return false;
379
+ }
380
+
381
+ bool MutationObservers::isInDocument_public(void* node_ptr) const {
382
+ return isInDocument(node_ptr);
383
+ }
384
+
385
+ bool MutationObservers::isAncestorOf(void* ancestor_ptr, void* node_ptr) const {
386
+ if (!ancestor_ptr || !node_ptr) return false;
387
+ lxb_dom_node_t* ancestor = static_cast<lxb_dom_node_t*>(ancestor_ptr);
388
+ lxb_dom_node_t* node = static_cast<lxb_dom_node_t*>(node_ptr);
389
+ lxb_dom_node_t* walk = node;
390
+ while (walk) {
391
+ if (walk == ancestor) return true;
392
+ walk = walk->parent;
393
+ }
394
+ return false;
395
+ }
396
+
397
+ // ── MutationObservers::scheduleDispatch ──────────────────────────────────────
398
+
399
+ void MutationObservers::scheduleDispatch(JSContext* ctx, RegisteredObserver& observer) {
400
+ if (observer.dispatch_scheduled || observer.disconnected) return;
401
+ observer.dispatch_scheduled = true;
402
+
403
+ JSValue carrier = JS_NewObjectClass(ctx, js_dispatch_carrier_class_id);
404
+ auto* job = new DispatchJob{ this, observer.id, observer.dispatch_generation };
405
+ JS_SetOpaque(carrier, job);
406
+
407
+ // Enqueue a job — the carrier JSValue is passed as argv[0]
408
+ JS_EnqueueJob(ctx, dispatch_trampoline, 1, &carrier);
409
+ JS_FreeValue(ctx, carrier);
410
+ }
411
+
412
+ // ── notifyChildList ───────────────────────────────────────────────────────────
413
+
414
+ void MutationObservers::notifyChildList(JSContext* ctx, void* target,
415
+ std::vector<void*> added,
416
+ std::vector<void*> removed,
417
+ void* prev_sibling, void* next_sibling) {
418
+ if (_observers.empty()) return;
419
+
420
+ for (auto& obs : _observers) {
421
+ if (obs.disconnected || !obs.options.childList) continue;
422
+ if (!obs.target) continue;
423
+
424
+ bool matches = (obs.target == target) ||
425
+ (obs.options.subtree && isAncestorOf(obs.target, target));
426
+ if (!matches) continue;
427
+
428
+ MutationRecord rec;
429
+ rec.type = "childList";
430
+ rec.target = target;
431
+ rec.addedNodes = added;
432
+ rec.removedNodes = removed;
433
+ rec.previousSibling = prev_sibling;
434
+ rec.nextSibling = next_sibling;
435
+ obs.queue.push_back(std::move(rec));
436
+ scheduleDispatch(ctx, obs);
437
+ }
438
+ }
439
+
440
+ // ── notifyAttribute ───────────────────────────────────────────────────────────
441
+
442
+ void MutationObservers::notifyAttribute(JSContext* ctx, void* target,
443
+ const std::string& attr_name,
444
+ std::optional<std::string> old_value) {
445
+ if (_observers.empty()) return;
446
+
447
+ for (auto& obs : _observers) {
448
+ if (obs.disconnected || !obs.options.attributes) continue;
449
+ if (!obs.target) continue;
450
+
451
+ bool matches = (obs.target == target) ||
452
+ (obs.options.subtree && isAncestorOf(obs.target, target));
453
+ if (!matches) continue;
454
+
455
+ // Check attributeFilter
456
+ if (obs.options.attributeFilter.has_value()) {
457
+ const auto& filters = obs.options.attributeFilter.value();
458
+ bool allowed = std::find(filters.begin(), filters.end(), attr_name) != filters.end();
459
+ if (!allowed) continue;
460
+ }
461
+
462
+ MutationRecord rec;
463
+ rec.type = "attributes";
464
+ rec.target = target;
465
+ rec.attributeName = attr_name;
466
+ rec.oldValue = obs.options.attributeOldValue ? old_value : std::nullopt;
467
+ obs.queue.push_back(std::move(rec));
468
+ scheduleDispatch(ctx, obs);
469
+ }
470
+ }
471
+
472
+ // ── notifyCharacterData ───────────────────────────────────────────────────────
473
+
474
+ void MutationObservers::notifyCharacterData(JSContext* ctx, void* target,
475
+ std::optional<std::string> old_value) {
476
+ if (_observers.empty()) return;
477
+
478
+ for (auto& obs : _observers) {
479
+ if (obs.disconnected || !obs.options.characterData) continue;
480
+ if (!obs.target) continue;
481
+
482
+ bool matches = (obs.target == target) ||
483
+ (obs.options.subtree && isAncestorOf(obs.target, target));
484
+ if (!matches) continue;
485
+
486
+ MutationRecord rec;
487
+ rec.type = "characterData";
488
+ rec.target = target;
489
+ rec.oldValue = obs.options.characterDataOldValue ? old_value : std::nullopt;
490
+ obs.queue.push_back(std::move(rec));
491
+ scheduleDispatch(ctx, obs);
492
+ }
493
+ }
494
+
495
+ // ── MutationObservers::disconnectDetachedTargets ──────────────────────────────
496
+
497
+ void MutationObservers::disconnectDetachedTargets(const std::vector<void*>& /*destroyed_nodes*/) {
498
+ if (_observers.empty()) return;
499
+ for (auto& obs : _observers) {
500
+ if (obs.disconnected || !obs.target) continue;
501
+ if (!isInDocument(obs.target)) {
502
+ obs.disconnected = true;
503
+ if (_active_count > 0) _active_count--;
504
+ obs.dispatch_scheduled = false;
505
+ for (auto& rec : obs.queue) {
506
+ rec.removedNodes.clear();
507
+ }
508
+ obs.queue.clear();
509
+ }
510
+ }
511
+ for (auto& obs : _observers) {
512
+ if (obs.disconnected) continue;
513
+ for (auto& rec : obs.queue) {
514
+ if (!rec.removedNodes.empty()) {
515
+ rec.removedNodes.erase(
516
+ std::remove_if(rec.removedNodes.begin(), rec.removedNodes.end(),
517
+ [this](void* n) { return !isInDocument(n); }),
518
+ rec.removedNodes.end());
519
+ }
520
+ }
521
+ }
522
+ }
523
+
524
+ // ── MutationObservers::clearAll ───────────────────────────────────────────────
525
+
526
+ void MutationObservers::clearAll(JSContext* ctx) {
527
+ for (auto& obs : _observers) {
528
+ if (obs.js_callback) {
529
+ JSValue* cb = static_cast<JSValue*>(obs.js_callback);
530
+ JS_FreeValue(ctx, *cb);
531
+ delete cb;
532
+ obs.js_callback = nullptr;
533
+ }
534
+ obs.queue.clear();
535
+ }
536
+ _observers.clear();
537
+ _active_count = 0;
538
+ }
539
+
540
+ // ── _observers_ref (used by trampoline) ──────────────────────────────────────
541
+
542
+ std::vector<RegisteredObserver>& MutationObservers::_observers_ref() {
543
+ return _observers;
544
+ }
545
+
546
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,127 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <vector>
5
+ #include <optional>
6
+ #include <memory>
7
+ #include <cstdint>
8
+
9
+ // Forward-declare the opaque JSContext type to avoid including quickjs.h here
10
+ typedef struct JSContext JSContext;
11
+
12
+ namespace margelo::nitro::nitrojsdom {
13
+
14
+ // ── MutationRecord ────────────────────────────────────────────────────────────
15
+
16
+ struct MutationRecord {
17
+ std::string type; // "childList" | "attributes" | "characterData"
18
+ void* target { nullptr }; // lxb_dom_node_t*
19
+ std::vector<void*> addedNodes; // lxb_dom_node_t* list
20
+ std::vector<void*> removedNodes; // lxb_dom_node_t* list
21
+ void* previousSibling { nullptr }; // lxb_dom_node_t*
22
+ void* nextSibling { nullptr }; // lxb_dom_node_t*
23
+ std::string attributeName;
24
+ std::optional<std::string> oldValue;
25
+ };
26
+
27
+ // ── ObserverInit ──────────────────────────────────────────────────────────────
28
+
29
+ struct ObserverInit {
30
+ bool childList { false };
31
+ bool attributes { false };
32
+ bool characterData { false };
33
+ bool subtree { false };
34
+ bool attributeOldValue { false };
35
+ bool characterDataOldValue { false };
36
+ std::optional<std::vector<std::string>> attributeFilter;
37
+ };
38
+
39
+ // ── RegisteredObserver ────────────────────────────────────────────────────────
40
+
41
+ struct RegisteredObserver {
42
+ uint32_t id;
43
+ void* target { nullptr }; // lxb_dom_node_t*
44
+ ObserverInit options;
45
+ void* js_callback { nullptr }; // JSValue* (heap-allocated, owned)
46
+ std::vector<MutationRecord> queue;
47
+ bool dispatch_scheduled { false };
48
+ bool disconnected { false };
49
+ uint32_t dispatch_generation { 0 };
50
+ };
51
+
52
+ // ── MutationObservers ─────────────────────────────────────────────────────────
53
+
54
+ class MutationObservers {
55
+ public:
56
+ MutationObservers() = default;
57
+ ~MutationObservers() = default;
58
+
59
+ // Called from DOMBindings::install to register the MutationObserver constructor
60
+ void install(JSContext* ctx, void* doc_root);
61
+
62
+ // Register an observer; returns its id
63
+ uint32_t addObserver(void* target, ObserverInit options, void* js_callback);
64
+
65
+ // Remove an observer and discard pending records
66
+ void disconnect(uint32_t observer_id);
67
+
68
+ // Flush and return pending records (without invoking callback)
69
+ std::vector<MutationRecord> takeRecords(uint32_t observer_id);
70
+
71
+ // Mutation notification hooks (called from DOMBindings after each mutation)
72
+ void notifyChildList(JSContext* ctx, void* target, std::vector<void*> added,
73
+ std::vector<void*> removed, void* prev_sibling, void* next_sibling);
74
+ void notifyAttribute(JSContext* ctx, void* target, const std::string& attr_name,
75
+ std::optional<std::string> old_value);
76
+ void notifyCharacterData(JSContext* ctx, void* target, std::optional<std::string> old_value);
77
+
78
+ // True when no active (non-disconnected) observers exist (fast-path check)
79
+ bool empty() const { return _active_count == 0; }
80
+
81
+ // True if at least one active observer requests attributeOldValue capture
82
+ bool hasAttributeOldValueObserver() const {
83
+ for (const auto& obs : _observers)
84
+ if (!obs.disconnected && obs.options.attributeOldValue) return true;
85
+ return false;
86
+ }
87
+
88
+ // True if at least one active observer requests characterDataOldValue capture
89
+ bool hasCharacterDataOldValueObserver() const {
90
+ for (const auto& obs : _observers)
91
+ if (!obs.disconnected && obs.options.characterDataOldValue) return true;
92
+ return false;
93
+ }
94
+
95
+ // Free all held JSValue callbacks — must be called before JS_FreeContext
96
+ void clearAll(JSContext* ctx);
97
+
98
+ // Store the document root pointer for ancestry checks
99
+ void setDocumentRoot(void* doc_root) { _doc_root = doc_root; }
100
+
101
+ // Update target + options for an existing observer (called from observe())
102
+ void updateObserver(uint32_t id, void* target, ObserverInit options);
103
+
104
+ // Disconnect any observer whose target is in the given set of destroyed nodes
105
+ void disconnectDetachedTargets(const std::vector<void*>& destroyed_nodes);
106
+
107
+ // Public wrappers around private helpers (needed by dispatch_trampoline)
108
+ bool isInDocument_public(void* node) const;
109
+ std::vector<RegisteredObserver>& _observers_ref();
110
+
111
+ private:
112
+ std::vector<RegisteredObserver> _observers;
113
+ uint32_t _next_id { 1 };
114
+ uint32_t _active_count { 0 }; // number of non-disconnected observers
115
+ void* _doc_root { nullptr };
116
+
117
+ // Returns true if node is still attached to _doc_root's tree
118
+ bool isInDocument(void* node) const;
119
+
120
+ // Returns true if ancestor is an ancestor-or-equal of node
121
+ bool isAncestorOf(void* ancestor, void* node) const;
122
+
123
+ // Schedule a microtask dispatch for the observer if not already scheduled
124
+ void scheduleDispatch(JSContext* ctx, RegisteredObserver& observer);
125
+ };
126
+
127
+ } // namespace margelo::nitro::nitrojsdom