@salve-software/react-native-nitro-jsdom 1.0.1 → 2.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 (66) hide show
  1. package/android/CMakeLists.txt +18 -0
  2. package/cpp/HybridHtmlSandbox.cpp +20 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +279 -67
  5. package/cpp/lexbor/LexborDocument.hpp +43 -10
  6. package/cpp/quickjs/DOMBindings.cpp +37 -1
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +101 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +56 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +32 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +50 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +128 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +164 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +290 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +25 -0
  17. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  18. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  19. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  20. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  22. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  24. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  25. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  26. package/cpp/quickjs/bindings/DocumentBindings.cpp +239 -4
  27. package/cpp/quickjs/bindings/ElementBindings.cpp +556 -44
  28. package/cpp/quickjs/bindings/EventBindings.cpp +414 -8
  29. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  30. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  31. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  32. package/cpp/quickjs/bindings/FetchBindings.cpp +45 -6
  33. package/cpp/quickjs/bindings/FormBindings.cpp +365 -0
  34. package/cpp/quickjs/bindings/FormBindings.hpp +20 -0
  35. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  36. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  37. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +228 -0
  38. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  39. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  40. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  41. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  42. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  43. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  44. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  45. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  46. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  47. package/cpp/quickjs/bindings/TimerBindings.cpp +120 -0
  48. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  49. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  50. package/cpp/quickjs/bindings/UrlBindings.cpp +295 -0
  51. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  52. package/cpp/quickjs/bindings/WindowBindings.cpp +552 -0
  53. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  54. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  55. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  56. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  57. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  58. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  59. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  60. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  61. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  62. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  63. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  64. package/package.json +17 -2
  65. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  66. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -1,6 +1,9 @@
1
1
  #include "WindowBindings.hpp"
2
+ #include "DOMExceptionBindings.hpp"
3
+ #include "EventBindings.hpp"
2
4
  #include "../DOMBindingsInternal.hpp"
3
5
  #include "../QuickJSRuntime.hpp"
6
+ #include <cstdlib>
4
7
  #include <cstring>
5
8
  #include <optional>
6
9
  #include <string>
@@ -10,6 +13,162 @@ namespace margelo::nitro::nitrojsdom {
10
13
 
11
14
  namespace {
12
15
 
16
+ // ── atob / btoa ────────────────────────────────────────────────────────────
17
+
18
+ const char kBase64Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
19
+
20
+ int8_t base64_decode_char(unsigned char c) {
21
+ if (c >= 'A' && c <= 'Z') return c - 'A';
22
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
23
+ if (c >= '0' && c <= '9') return c - '0' + 52;
24
+ if (c == '+') return 62;
25
+ if (c == '/') return 63;
26
+ return -1;
27
+ }
28
+
29
+ bool is_ascii_whitespace(char c) {
30
+ return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
31
+ }
32
+
33
+ JSValue js_window_btoa(JSContext* ctx, JSValue, int argc, JSValue* argv) {
34
+ if (argc < 1) return JS_ThrowTypeError(ctx, "btoa() requires 1 argument");
35
+ JSValue str_val = JS_ToString(ctx, argv[0]);
36
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
37
+ size_t len = 0;
38
+ const char* s = JS_ToCStringLen(ctx, &len, str_val);
39
+ if (!s) { JS_FreeValue(ctx, str_val); return JS_EXCEPTION; }
40
+
41
+ // JS_ToCStringLen yields UTF-8 (length-aware, so embedded NULs survive);
42
+ // btoa operates on Latin1 code units, so decode the UTF-8 back into code
43
+ // points and reject anything outside 0x00-0xFF.
44
+ std::vector<uint8_t> bytes;
45
+ bytes.reserve(len);
46
+ bool out_of_range = false;
47
+ for (size_t i = 0; i < len && !out_of_range;) {
48
+ unsigned char c = static_cast<unsigned char>(s[i]);
49
+ uint32_t codepoint;
50
+ size_t seq_len;
51
+ if (c < 0x80) { codepoint = c; seq_len = 1; }
52
+ else if ((c & 0xE0) == 0xC0 && i + 1 < len) { codepoint = c & 0x1F; seq_len = 2; }
53
+ else if ((c & 0xF0) == 0xE0 && i + 2 < len) { codepoint = c & 0x0F; seq_len = 3; }
54
+ else { out_of_range = true; break; }
55
+ for (size_t k = 1; k < seq_len; k++) {
56
+ codepoint = (codepoint << 6) | (static_cast<unsigned char>(s[i + k]) & 0x3F);
57
+ }
58
+ if (codepoint > 0xFF) { out_of_range = true; break; }
59
+ bytes.push_back(static_cast<uint8_t>(codepoint));
60
+ i += seq_len;
61
+ }
62
+ JS_FreeCString(ctx, s);
63
+ JS_FreeValue(ctx, str_val);
64
+
65
+ if (out_of_range) {
66
+ return throw_dom_exception(ctx, "InvalidCharacterError",
67
+ "The string to be encoded contains characters outside of the Latin1 range.");
68
+ }
69
+
70
+ std::string out;
71
+ out.reserve(((bytes.size() + 2) / 3) * 4);
72
+ size_t i = 0;
73
+ while (i + 3 <= bytes.size()) {
74
+ uint32_t n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
75
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
76
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
77
+ out += kBase64Alphabet[(n >> 6) & 0x3F];
78
+ out += kBase64Alphabet[n & 0x3F];
79
+ i += 3;
80
+ }
81
+ size_t remaining = bytes.size() - i;
82
+ if (remaining == 1) {
83
+ uint32_t n = bytes[i] << 16;
84
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
85
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
86
+ out += "==";
87
+ } else if (remaining == 2) {
88
+ uint32_t n = (bytes[i] << 16) | (bytes[i + 1] << 8);
89
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
90
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
91
+ out += kBase64Alphabet[(n >> 6) & 0x3F];
92
+ out += "=";
93
+ }
94
+
95
+ return JS_NewStringLen(ctx, out.c_str(), out.size());
96
+ }
97
+
98
+ JSValue js_window_atob(JSContext* ctx, JSValue, int argc, JSValue* argv) {
99
+ if (argc < 1) return JS_ThrowTypeError(ctx, "atob() requires 1 argument");
100
+ JSValue str_val = JS_ToString(ctx, argv[0]);
101
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
102
+ size_t len = 0;
103
+ const char* s = JS_ToCStringLen(ctx, &len, str_val);
104
+ if (!s) { JS_FreeValue(ctx, str_val); return JS_EXCEPTION; }
105
+
106
+ std::string data;
107
+ data.reserve(len);
108
+ for (size_t i = 0; i < len; i++) {
109
+ if (!is_ascii_whitespace(s[i])) data += s[i];
110
+ }
111
+ JS_FreeCString(ctx, s);
112
+ JS_FreeValue(ctx, str_val);
113
+
114
+ if (data.size() % 4 == 0) {
115
+ if (data.size() >= 2 && data[data.size() - 1] == '=' && data[data.size() - 2] == '=') {
116
+ data.resize(data.size() - 2);
117
+ } else if (!data.empty() && data.back() == '=') {
118
+ data.resize(data.size() - 1);
119
+ }
120
+ }
121
+
122
+ if (data.size() % 4 == 1) {
123
+ return throw_dom_exception(ctx, "InvalidCharacterError",
124
+ "The string to be decoded is not correctly encoded.");
125
+ }
126
+ for (char c : data) {
127
+ if (base64_decode_char(static_cast<unsigned char>(c)) < 0) {
128
+ return throw_dom_exception(ctx, "InvalidCharacterError",
129
+ "The string to be decoded is not correctly encoded.");
130
+ }
131
+ }
132
+
133
+ std::string out;
134
+ out.reserve((data.size() / 4) * 3 + 3);
135
+ size_t i = 0;
136
+ while (i + 4 <= data.size()) {
137
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12) |
138
+ (base64_decode_char(data[i + 2]) << 6) | base64_decode_char(data[i + 3]);
139
+ out += static_cast<char>((n >> 16) & 0xFF);
140
+ out += static_cast<char>((n >> 8) & 0xFF);
141
+ out += static_cast<char>(n & 0xFF);
142
+ i += 4;
143
+ }
144
+ size_t remaining = data.size() - i;
145
+ if (remaining == 2) {
146
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12);
147
+ out += static_cast<char>((n >> 16) & 0xFF);
148
+ } else if (remaining == 3) {
149
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12) |
150
+ (base64_decode_char(data[i + 2]) << 6);
151
+ out += static_cast<char>((n >> 16) & 0xFF);
152
+ out += static_cast<char>((n >> 8) & 0xFF);
153
+ }
154
+
155
+ // atob's result is a "binary string" — each output char code is one decoded
156
+ // byte (0x00-0xFF). JS_NewStringLen expects UTF-8 input, so each byte must
157
+ // be re-encoded as the UTF-8 sequence for that same code point (1 byte for
158
+ // 0x00-0x7F, 2 bytes for 0x80-0xFF) to round-trip through QuickJS correctly.
159
+ std::string utf8;
160
+ utf8.reserve(out.size() * 2);
161
+ for (unsigned char byte : out) {
162
+ if (byte < 0x80) {
163
+ utf8 += static_cast<char>(byte);
164
+ } else {
165
+ utf8 += static_cast<char>(0xC0 | (byte >> 6));
166
+ utf8 += static_cast<char>(0x80 | (byte & 0x3F));
167
+ }
168
+ }
169
+ return JS_NewStringLen(ctx, utf8.c_str(), utf8.size());
170
+ }
171
+
13
172
  // ── console ────────────────────────────────────────────────────────────────
14
173
 
15
174
  JSValue js_console_method(JSContext* ctx, JSValue, int argc, JSValue* argv, int magic) {
@@ -33,6 +192,23 @@ JSValue js_console_method(JSContext* ctx, JSValue, int argc, JSValue* argv, int
33
192
  return JS_UNDEFINED;
34
193
  }
35
194
 
195
+ JSValue js_window_reportError(JSContext* ctx, JSValue, int argc, JSValue* argv) {
196
+ JSValue error_value = argc >= 1 ? argv[0] : JS_UNDEFINED;
197
+ std::string message = "Uncaught";
198
+ if (argc >= 1) {
199
+ JSValue msg_prop = JS_GetPropertyStr(ctx, argv[0], "message");
200
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
201
+ const char* s = JS_ToCString(ctx, msg_prop);
202
+ if (s) { message = s; JS_FreeCString(ctx, s); }
203
+ } else if (JS_IsException(msg_prop)) {
204
+ JS_FreeValue(ctx, JS_GetException(ctx));
205
+ }
206
+ JS_FreeValue(ctx, msg_prop);
207
+ }
208
+ EventBindings::dispatchErrorEvent(ctx, message, error_value);
209
+ return JS_UNDEFINED;
210
+ }
211
+
36
212
  // ── window.alert / confirm / prompt ───────────────────────────────────────────
37
213
 
38
214
  JSValue js_window_alert(JSContext* ctx, JSValue, int argc, JSValue* argv) {
@@ -181,6 +357,329 @@ const char* kLocationBootstrapScript = R"JS(
181
357
  })();
182
358
  )JS";
183
359
 
360
+ // ── crypto.getRandomValues ────────────────────────────────────────────────────
361
+
362
+ JSValue js_native_random_bytes(JSContext* ctx, JSValue, int argc, JSValue* argv) {
363
+ int32_t count = 0;
364
+ if (argc >= 1) JS_ToInt32(ctx, &count, argv[0]);
365
+ if (count < 0) count = 0;
366
+
367
+ std::vector<uint8_t> bytes(static_cast<size_t>(count));
368
+ if (count > 0) arc4random_buf(bytes.data(), bytes.size());
369
+
370
+ JSValue arr = JS_NewArray(ctx);
371
+ for (int32_t i = 0; i < count; i++) {
372
+ JS_SetPropertyUint32(ctx, arr, static_cast<uint32_t>(i), JS_NewInt32(ctx, bytes[static_cast<size_t>(i)]));
373
+ }
374
+ return arr;
375
+ }
376
+
377
+ const char* kCryptoBootstrapScript = R"JS(
378
+ (function() {
379
+ globalThis.crypto = globalThis.crypto || {};
380
+ globalThis.crypto.getRandomValues = function(typedArray) {
381
+ if (!typedArray || typeof typedArray.byteLength !== 'number' || typedArray instanceof BigInt64Array || typedArray instanceof BigUint64Array) {
382
+ throw new TypeError('crypto.getRandomValues() requires an integer TypedArray argument');
383
+ }
384
+ if (typedArray.byteLength > 65536) {
385
+ var err = new Error('crypto.getRandomValues() can only fill up to 65536 bytes at a time');
386
+ err.name = 'QuotaExceededError';
387
+ throw err;
388
+ }
389
+ var bytes = __nativeRandomBytes(typedArray.byteLength);
390
+ var view = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
391
+ for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
392
+ return typedArray;
393
+ };
394
+ globalThis.crypto.randomUUID = function() {
395
+ var bytes = __nativeRandomBytes(16);
396
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
397
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
398
+ var hex = [];
399
+ for (var i = 0; i < 16; i++) {
400
+ var h = bytes[i].toString(16);
401
+ hex.push(h.length === 1 ? '0' + h : h);
402
+ }
403
+ return hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' +
404
+ hex.slice(6, 8).join('') + '-' + hex.slice(8, 10).join('') + '-' + hex.slice(10, 16).join('');
405
+ };
406
+ })();
407
+ )JS";
408
+
409
+ // ── console ergonomics (group/table/assert/trace/count) ──────────────────────
410
+ // Layered in pure JS on top of the native log/warn/error/info/debug methods
411
+ // above rather than adding new native levels — none of these need anything
412
+ // the onConsole callback can't already receive as stringified args.
413
+
414
+ const char* kConsoleExtrasBootstrapScript = R"JS(
415
+ (function() {
416
+ var counts = Object.create(null);
417
+
418
+ console.group = function() {
419
+ console.log.apply(console, arguments);
420
+ };
421
+ console.groupCollapsed = console.group;
422
+ console.groupEnd = function() {};
423
+
424
+ console.trace = function() {
425
+ var args = Array.prototype.slice.call(arguments);
426
+ args.unshift('Trace:');
427
+ console.log.apply(console, args);
428
+ };
429
+
430
+ console.assert = function(condition) {
431
+ if (condition) return;
432
+ var args = Array.prototype.slice.call(arguments, 1);
433
+ args.unshift('Assertion failed:');
434
+ console.error.apply(console, args);
435
+ };
436
+
437
+ console.table = function(data) {
438
+ try {
439
+ console.log(JSON.stringify(data));
440
+ } catch (e) {
441
+ console.log(String(data));
442
+ }
443
+ };
444
+
445
+ console.count = function(label) {
446
+ label = label === undefined ? 'default' : String(label);
447
+ counts[label] = (counts[label] || 0) + 1;
448
+ console.log(label + ': ' + counts[label]);
449
+ };
450
+
451
+ console.countReset = function(label) {
452
+ label = label === undefined ? 'default' : String(label);
453
+ counts[label] = 0;
454
+ };
455
+ })();
456
+ )JS";
457
+
458
+ // ── navigator ──────────────────────────────────────────────────────────────
459
+
460
+ const char* kNavigatorBootstrapScript = R"JS(
461
+ (function() {
462
+ var navigator = {
463
+ userAgent: 'Mozilla/5.0 (ReactNative) react-native-nitro-jsdom',
464
+ platform: 'ReactNative',
465
+ language: 'en-US',
466
+ languages: ['en-US'],
467
+ onLine: true,
468
+ cookieEnabled: false,
469
+ hardwareConcurrency: 1,
470
+ };
471
+ navigator.sendBeacon = function(url, data) {
472
+ try {
473
+ fetch(url, { method: 'POST', body: data }).catch(function() {});
474
+ } catch (e) {
475
+ return false;
476
+ }
477
+ return true;
478
+ };
479
+
480
+ // jsdom itself doesn't implement the Clipboard API at all (no OS clipboard
481
+ // to back it, same reasoning as window.history/getSelection). This is an
482
+ // in-memory stand-in purely so 'copy discount code'-style embedded widgets
483
+ // that call navigator.clipboard.writeText() directly don't throw — reads
484
+ // back whatever was last written, nothing more.
485
+ var clipboardText = '';
486
+ navigator.clipboard = {
487
+ writeText: function(text) {
488
+ clipboardText = String(text);
489
+ return Promise.resolve();
490
+ },
491
+ readText: function() {
492
+ return Promise.resolve(clipboardText);
493
+ },
494
+ };
495
+
496
+ globalThis.navigator = navigator;
497
+ })();
498
+ )JS";
499
+
500
+ // ── structuredClone ────────────────────────────────────────────────────────
501
+
502
+ const char* kStructuredCloneBootstrapScript = R"JS(
503
+ (function() {
504
+ globalThis.structuredClone = function(value) {
505
+ var seen = new Map();
506
+ function clone(v) {
507
+ if (v === null || typeof v !== 'object') {
508
+ if (typeof v === 'function' || typeof v === 'symbol') {
509
+ throw new DOMException(String(v) + ' could not be cloned.', 'DataCloneError');
510
+ }
511
+ return v;
512
+ }
513
+ if (seen.has(v)) return seen.get(v);
514
+ if (v instanceof Date) return new Date(v.getTime());
515
+ if (v instanceof RegExp) return new RegExp(v.source, v.flags);
516
+ if (Array.isArray(v)) {
517
+ var arr = [];
518
+ seen.set(v, arr);
519
+ for (var i = 0; i < v.length; i++) arr.push(clone(v[i]));
520
+ return arr;
521
+ }
522
+ if (v instanceof Map) {
523
+ var m = new Map();
524
+ seen.set(v, m);
525
+ v.forEach(function(val, key) { m.set(clone(key), clone(val)); });
526
+ return m;
527
+ }
528
+ if (v instanceof Set) {
529
+ var s = new Set();
530
+ seen.set(v, s);
531
+ v.forEach(function(val) { s.add(clone(val)); });
532
+ return s;
533
+ }
534
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(v)) {
535
+ return new v.constructor(v);
536
+ }
537
+ if (typeof ArrayBuffer !== 'undefined' && v instanceof ArrayBuffer) {
538
+ return v.slice(0);
539
+ }
540
+ var proto = Object.getPrototypeOf(v);
541
+ if (proto !== Object.prototype && proto !== null) {
542
+ throw new DOMException('The object could not be cloned.', 'DataCloneError');
543
+ }
544
+ var out = {};
545
+ seen.set(v, out);
546
+ for (var key in v) {
547
+ if (Object.prototype.hasOwnProperty.call(v, key)) out[key] = clone(v[key]);
548
+ }
549
+ return out;
550
+ }
551
+ return clone(value);
552
+ };
553
+ })();
554
+ )JS";
555
+
556
+ // ── window.history ────────────────────────────────────────────────────────
557
+ // No real page navigation exists in this sandbox, so History tracks its own
558
+ // in-memory entry stack rather than session history. pushState/replaceState
559
+ // never fire popstate (per spec); go/back/forward do, since those are the
560
+ // events CMS-widget routers actually listen for. `title` is accepted but
561
+ // ignored (same as jsdom — nothing renders a document title). State is
562
+ // structuredClone()'d on write (per spec — matches pushState/replaceState's
563
+ // documented "serializable" semantics), so callers mutating their object
564
+ // after the call can't leak into a stored entry. `go()`'s delta is truncated
565
+ // to an integer so a fractional/NaN input can't produce a non-integer
566
+ // `_index`.
567
+
568
+ const char* kHistoryBootstrapScript = R"JS(
569
+ (function() {
570
+ function History() {
571
+ this._entries = [null];
572
+ this._index = 0;
573
+ }
574
+ Object.defineProperty(History.prototype, 'length', {
575
+ get: function() { return this._entries.length; },
576
+ enumerable: true,
577
+ });
578
+ Object.defineProperty(History.prototype, 'state', {
579
+ get: function() { return this._entries[this._index]; },
580
+ enumerable: true,
581
+ });
582
+ function cloneState(state) {
583
+ return state === undefined ? null : structuredClone(state);
584
+ }
585
+ History.prototype.pushState = function(state, _title, url) {
586
+ this._entries = this._entries.slice(0, this._index + 1);
587
+ this._entries.push(cloneState(state));
588
+ this._index++;
589
+ if (url !== undefined && url !== null) location.href = url;
590
+ };
591
+ History.prototype.replaceState = function(state, _title, url) {
592
+ this._entries[this._index] = cloneState(state);
593
+ if (url !== undefined && url !== null) location.href = url;
594
+ };
595
+ History.prototype.go = function(delta) {
596
+ delta = delta === undefined ? 0 : Math.trunc(Number(delta) || 0);
597
+ if (delta === 0) return;
598
+ var next = this._index + delta;
599
+ if (next < 0 || next > this._entries.length - 1) return;
600
+ this._index = next;
601
+ var ev = new Event('popstate');
602
+ ev.state = this._entries[this._index];
603
+ dispatchEvent(ev);
604
+ };
605
+ History.prototype.back = function() { this.go(-1); };
606
+ History.prototype.forward = function() { this.go(1); };
607
+
608
+ globalThis.History = History;
609
+ globalThis.history = new History();
610
+ })();
611
+ )JS";
612
+
613
+ // ── window.getSelection ──────────────────────────────────────────────────
614
+ // No layout engine backs this sandbox, so there is nothing to select — this
615
+ // mirrors matchMedia's stance: an always-empty Selection rather than a
616
+ // missing global, so defensive `window.getSelection().toString()` checks in
617
+ // third-party scripts don't crash the sandbox.
618
+
619
+ const char* kSelectionBootstrapScript = R"JS(
620
+ (function() {
621
+ function Selection() {}
622
+ Object.defineProperty(Selection.prototype, 'rangeCount', { get: function() { return 0; }, enumerable: true });
623
+ Object.defineProperty(Selection.prototype, 'isCollapsed', { get: function() { return true; }, enumerable: true });
624
+ Object.defineProperty(Selection.prototype, 'anchorNode', { get: function() { return null; }, enumerable: true });
625
+ Object.defineProperty(Selection.prototype, 'anchorOffset', { get: function() { return 0; }, enumerable: true });
626
+ Object.defineProperty(Selection.prototype, 'focusNode', { get: function() { return null; }, enumerable: true });
627
+ Object.defineProperty(Selection.prototype, 'focusOffset', { get: function() { return 0; }, enumerable: true });
628
+ Object.defineProperty(Selection.prototype, 'type', { get: function() { return 'None'; }, enumerable: true });
629
+ Selection.prototype.toString = function() { return ''; };
630
+ Selection.prototype.removeAllRanges = function() {};
631
+ Selection.prototype.collapse = function() {};
632
+ Selection.prototype.collapseToStart = function() {};
633
+ Selection.prototype.collapseToEnd = function() {};
634
+ Selection.prototype.selectAllChildren = function() {};
635
+ Selection.prototype.addRange = function() {};
636
+ Selection.prototype.containsNode = function() { return false; };
637
+ Selection.prototype.getRangeAt = function(index) {
638
+ throw new DOMException(
639
+ "Failed to execute 'getRangeAt' on 'Selection': " + index + ' is not a valid index.', 'IndexSizeError');
640
+ };
641
+ Selection.prototype.empty = Selection.prototype.removeAllRanges;
642
+
643
+ var selectionInstance = new Selection();
644
+ globalThis.Selection = Selection;
645
+ globalThis.getSelection = function() { return selectionInstance; };
646
+ })();
647
+ )JS";
648
+
649
+ // ── matchMedia ─────────────────────────────────────────────────────────────
650
+ // No layout engine backs this sandbox, so every query reports "no match" —
651
+ // mirrors jsdom's own stance that layout-dependent APIs return inert defaults
652
+ // rather than throwing.
653
+
654
+ const char* kMatchMediaBootstrapScript = R"JS(
655
+ (function() {
656
+ function MediaQueryList(media) {
657
+ this.media = String(media === undefined ? '' : media);
658
+ this.matches = false;
659
+ this.onchange = null;
660
+ this._listeners = [];
661
+ }
662
+ MediaQueryList.prototype.addListener = function(cb) {
663
+ if (typeof cb === 'function' && this._listeners.indexOf(cb) === -1) this._listeners.push(cb);
664
+ };
665
+ MediaQueryList.prototype.removeListener = function(cb) {
666
+ var idx = this._listeners.indexOf(cb);
667
+ if (idx !== -1) this._listeners.splice(idx, 1);
668
+ };
669
+ MediaQueryList.prototype.addEventListener = function(type, cb) {
670
+ if (type === 'change') this.addListener(cb);
671
+ };
672
+ MediaQueryList.prototype.removeEventListener = function(type, cb) {
673
+ if (type === 'change') this.removeListener(cb);
674
+ };
675
+ MediaQueryList.prototype.dispatchEvent = function() { return true; };
676
+
677
+ globalThis.matchMedia = function(query) {
678
+ return new MediaQueryList(query);
679
+ };
680
+ })();
681
+ )JS";
682
+
184
683
  } // namespace
185
684
 
186
685
  void WindowBindings::install(JSContext* ctx) {
@@ -189,6 +688,9 @@ void WindowBindings::install(JSContext* ctx) {
189
688
  JS_SetPropertyStr(ctx, global, "alert", JS_NewCFunction(ctx, js_window_alert, "alert", 1));
190
689
  JS_SetPropertyStr(ctx, global, "confirm", JS_NewCFunction(ctx, js_window_confirm, "confirm", 1));
191
690
  JS_SetPropertyStr(ctx, global, "prompt", JS_NewCFunction(ctx, js_window_prompt, "prompt", 2));
691
+ JS_SetPropertyStr(ctx, global, "reportError", JS_NewCFunction(ctx, js_window_reportError, "reportError", 1));
692
+ JS_SetPropertyStr(ctx, global, "atob", JS_NewCFunction(ctx, js_window_atob, "atob", 1));
693
+ JS_SetPropertyStr(ctx, global, "btoa", JS_NewCFunction(ctx, js_window_btoa, "btoa", 1));
192
694
 
193
695
  JSValue console_obj = JS_NewObject(ctx);
194
696
  JS_SetPropertyStr(ctx, console_obj, "log", JS_NewCFunctionMagic(ctx, js_console_method, "log", 0, JS_CFUNC_generic_magic, 0));
@@ -197,6 +699,7 @@ void WindowBindings::install(JSContext* ctx) {
197
699
  JS_SetPropertyStr(ctx, console_obj, "info", JS_NewCFunctionMagic(ctx, js_console_method, "info", 0, JS_CFUNC_generic_magic, 3));
198
700
  JS_SetPropertyStr(ctx, console_obj, "debug", JS_NewCFunctionMagic(ctx, js_console_method, "debug", 0, JS_CFUNC_generic_magic, 4));
199
701
  JS_SetPropertyStr(ctx, global, "console", console_obj);
702
+ JS_SetPropertyStr(ctx, global, "__nativeRandomBytes", JS_NewCFunction(ctx, js_native_random_bytes, "__nativeRandomBytes", 1));
200
703
 
201
704
  JS_FreeValue(ctx, global);
202
705
 
@@ -206,6 +709,55 @@ void WindowBindings::install(JSContext* ctx) {
206
709
  JS_FreeValue(ctx, JS_GetException(ctx));
207
710
  }
208
711
  JS_FreeValue(ctx, location_result);
712
+
713
+ JSValue history_result = JS_Eval(ctx, kHistoryBootstrapScript, strlen(kHistoryBootstrapScript),
714
+ "<history-bootstrap>", JS_EVAL_TYPE_GLOBAL);
715
+ if (JS_IsException(history_result)) {
716
+ JS_FreeValue(ctx, JS_GetException(ctx));
717
+ }
718
+ JS_FreeValue(ctx, history_result);
719
+
720
+ JSValue selection_result = JS_Eval(ctx, kSelectionBootstrapScript, strlen(kSelectionBootstrapScript),
721
+ "<selection-bootstrap>", JS_EVAL_TYPE_GLOBAL);
722
+ if (JS_IsException(selection_result)) {
723
+ JS_FreeValue(ctx, JS_GetException(ctx));
724
+ }
725
+ JS_FreeValue(ctx, selection_result);
726
+
727
+ JSValue console_extras_result = JS_Eval(ctx, kConsoleExtrasBootstrapScript, strlen(kConsoleExtrasBootstrapScript),
728
+ "<console-extras-bootstrap>", JS_EVAL_TYPE_GLOBAL);
729
+ if (JS_IsException(console_extras_result)) {
730
+ JS_FreeValue(ctx, JS_GetException(ctx));
731
+ }
732
+ JS_FreeValue(ctx, console_extras_result);
733
+
734
+ JSValue crypto_result = JS_Eval(ctx, kCryptoBootstrapScript, strlen(kCryptoBootstrapScript),
735
+ "<crypto-bootstrap>", JS_EVAL_TYPE_GLOBAL);
736
+ if (JS_IsException(crypto_result)) {
737
+ JS_FreeValue(ctx, JS_GetException(ctx));
738
+ }
739
+ JS_FreeValue(ctx, crypto_result);
740
+
741
+ JSValue navigator_result = JS_Eval(ctx, kNavigatorBootstrapScript, strlen(kNavigatorBootstrapScript),
742
+ "<navigator-bootstrap>", JS_EVAL_TYPE_GLOBAL);
743
+ if (JS_IsException(navigator_result)) {
744
+ JS_FreeValue(ctx, JS_GetException(ctx));
745
+ }
746
+ JS_FreeValue(ctx, navigator_result);
747
+
748
+ JSValue match_media_result = JS_Eval(ctx, kMatchMediaBootstrapScript, strlen(kMatchMediaBootstrapScript),
749
+ "<match-media-bootstrap>", JS_EVAL_TYPE_GLOBAL);
750
+ if (JS_IsException(match_media_result)) {
751
+ JS_FreeValue(ctx, JS_GetException(ctx));
752
+ }
753
+ JS_FreeValue(ctx, match_media_result);
754
+
755
+ JSValue structured_clone_result = JS_Eval(ctx, kStructuredCloneBootstrapScript, strlen(kStructuredCloneBootstrapScript),
756
+ "<structured-clone-bootstrap>", JS_EVAL_TYPE_GLOBAL);
757
+ if (JS_IsException(structured_clone_result)) {
758
+ JS_FreeValue(ctx, JS_GetException(ctx));
759
+ }
760
+ JS_FreeValue(ctx, structured_clone_result);
209
761
  }
210
762
 
211
763
  } // namespace margelo::nitro::nitrojsdom
@@ -1,8 +1,9 @@
1
1
  #pragma once
2
2
 
3
3
  // window.alert/confirm/prompt (bridged to onAlert/onConfirm/onPrompt native
4
- // callbacks), console.log/warn/error/info/debug, and the window.location
5
- // bootstrap script.
4
+ // callbacks), console.log/warn/error/info/debug, and the window.location,
5
+ // window.history, window.getSelection, crypto, navigator, matchMedia, and
6
+ // structuredClone bootstrap scripts.
6
7
 
7
8
  #include "quickjs.h"
8
9
 
@@ -0,0 +1,44 @@
1
+ #include "XmlSerializerBindings.hpp"
2
+ #include "../DOMBindingsInternal.hpp"
3
+ #include <string>
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ namespace {
8
+
9
+ JSClassID js_xml_serializer_class_id = 0;
10
+ JSClassDef js_xml_serializer_class = { "XMLSerializer", .finalizer = nullptr };
11
+
12
+ JSValue js_xmlserializer_constructor(JSContext* ctx, JSValue, int, JSValue*) {
13
+ return JS_NewObjectClass(ctx, js_xml_serializer_class_id);
14
+ }
15
+
16
+ JSValue js_xmlserializer_serializeToString(JSContext* ctx, JSValue, int argc, JSValue* argv) {
17
+ if (argc < 1) return JS_NewString(ctx, "");
18
+ auto* node = unwrap_node(ctx, argv[0]);
19
+ if (!node) return JS_NewString(ctx, "");
20
+ std::string result = serialize_node(node);
21
+ return JS_NewStringLen(ctx, result.data(), result.size());
22
+ }
23
+
24
+ } // namespace
25
+
26
+ void XmlSerializerBindings::install(JSContext* ctx) {
27
+ if (js_xml_serializer_class_id == 0) JS_NewClassID(&js_xml_serializer_class_id);
28
+ JS_NewClass(JS_GetRuntime(ctx), js_xml_serializer_class_id, &js_xml_serializer_class);
29
+
30
+ JSValue proto = JS_NewObject(ctx);
31
+ JS_SetPropertyStr(ctx, proto, "serializeToString",
32
+ JS_NewCFunction(ctx, js_xmlserializer_serializeToString, "serializeToString", 1));
33
+ JS_SetClassProto(ctx, js_xml_serializer_class_id, JS_DupValue(ctx, proto));
34
+
35
+ JSValue ctor = JS_NewCFunction2(ctx, js_xmlserializer_constructor, "XMLSerializer", 0, JS_CFUNC_constructor, 0);
36
+ JS_SetConstructor(ctx, ctor, proto);
37
+ JS_FreeValue(ctx, proto);
38
+
39
+ JSValue global = JS_GetGlobalObject(ctx);
40
+ JS_SetPropertyStr(ctx, global, "XMLSerializer", ctor);
41
+ JS_FreeValue(ctx, global);
42
+ }
43
+
44
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers globalThis.XMLSerializer with serializeToString(node), backed by
8
+ // the same serialize_node() helper Element.prototype.outerHTML already uses
9
+ // (DOMBindingsInternal.hpp) — serializes the given node and its subtree.
10
+ //
11
+ // Scope: only serializeToString(node) is implemented (DOMParser.parseFromString
12
+ // is out of scope — see docs/overview.md's v0.10 notes, it would need a real
13
+ // second Document). `node` must be a wrapped Element/Node/ShadowRoot (i.e. not
14
+ // `document` itself, which is a plain JS object with no native node behind
15
+ // it) — pass document.documentElement instead, same as any other subtree
16
+ // serialization in this sandbox.
17
+ //
18
+ // Must run after ElementBindings (unwrap_node/serialize_node need
19
+ // js_element_class_id/js_node_class_id to exist).
20
+ struct XmlSerializerBindings {
21
+ static void install(JSContext* ctx);
22
+ };
23
+
24
+ } // namespace margelo::nitro::nitrojsdom
@@ -39,7 +39,7 @@ class JSDOM {
39
39
  */
40
40
  static create(html, options) {
41
41
  const sandbox = _reactNativeNitroModules.NitroModules.createHybridObject('HtmlSandbox');
42
- sandbox.initialize(html, options?.runScripts ?? true, options?.url ?? 'about:blank');
42
+ sandbox.initialize(html, options?.runScripts ?? true, options?.url ?? 'about:blank', options?.pretendToBeVisual ?? false);
43
43
  if (options?.onConsole) {
44
44
  const cb = options.onConsole;
45
45
  sandbox.setConsoleCallback((level, args) => cb(level, args));