@salve-software/react-native-nitro-jsdom 1.0.0 → 1.1.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 (57) hide show
  1. package/README.md +1 -1
  2. package/android/CMakeLists.txt +14 -0
  3. package/cpp/HybridHtmlSandbox.cpp +16 -3
  4. package/cpp/HybridHtmlSandbox.hpp +1 -1
  5. package/cpp/lexbor/LexborDocument.cpp +217 -67
  6. package/cpp/lexbor/LexborDocument.hpp +29 -10
  7. package/cpp/quickjs/DOMBindings.cpp +28 -0
  8. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  9. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  10. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  11. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  12. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  13. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  14. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  15. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  17. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
  18. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  19. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  20. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  22. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  24. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  25. package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
  26. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  27. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  28. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  29. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  30. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  32. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  34. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  35. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  36. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  38. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  40. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  41. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  42. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  43. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  44. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  46. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  48. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  50. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  51. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  53. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  54. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  55. package/package.json +2 -2
  56. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  57. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -0,0 +1,286 @@
1
+ #include "UrlBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kUrlBootstrapScript = R"JS(
9
+ (function() {
10
+ function encodeComponent(str) {
11
+ return encodeURIComponent(str).replace(/%20/g, '+').replace(/[!'()*]/g, function(c) {
12
+ return '%' + c.charCodeAt(0).toString(16).toUpperCase();
13
+ });
14
+ }
15
+ function decodeComponent(str) {
16
+ return decodeURIComponent(str.replace(/\+/g, ' '));
17
+ }
18
+
19
+ function parsePairs(search) {
20
+ var pairs = [];
21
+ var s = search && search.charAt(0) === '?' ? search.slice(1) : (search || '');
22
+ if (!s) return pairs;
23
+ var parts = s.split('&');
24
+ for (var i = 0; i < parts.length; i++) {
25
+ var part = parts[i];
26
+ if (!part) continue;
27
+ var eq = part.indexOf('=');
28
+ var k, v;
29
+ if (eq === -1) { k = part; v = ''; } else { k = part.slice(0, eq); v = part.slice(eq + 1); }
30
+ pairs.push([decodeComponent(k), decodeComponent(v)]);
31
+ }
32
+ return pairs;
33
+ }
34
+
35
+ function URLSearchParams(init) {
36
+ this._pairs = [];
37
+ this._onChange = null;
38
+ if (init === undefined || init === null || init === '') {
39
+ } else if (typeof init === 'string') {
40
+ this._pairs = parsePairs(init);
41
+ } else if (init instanceof URLSearchParams) {
42
+ for (var i = 0; i < init._pairs.length; i++) this._pairs.push(init._pairs[i].slice());
43
+ } else if (Array.isArray(init)) {
44
+ for (var i = 0; i < init.length; i++) this._pairs.push([String(init[i][0]), String(init[i][1])]);
45
+ } else if (typeof init === 'object') {
46
+ for (var k in init) {
47
+ if (Object.prototype.hasOwnProperty.call(init, k)) this._pairs.push([String(k), String(init[k])]);
48
+ }
49
+ }
50
+ }
51
+
52
+ URLSearchParams.prototype._notify = function() { if (this._onChange) this._onChange(); };
53
+
54
+ URLSearchParams.prototype.append = function(name, value) {
55
+ this._pairs.push([String(name), String(value)]);
56
+ this._notify();
57
+ };
58
+ URLSearchParams.prototype.delete = function(name, value) {
59
+ name = String(name);
60
+ var hasValue = value !== undefined;
61
+ var v = hasValue ? String(value) : null;
62
+ var out = [];
63
+ for (var i = 0; i < this._pairs.length; i++) {
64
+ var p = this._pairs[i];
65
+ if (p[0] === name && (!hasValue || p[1] === v)) continue;
66
+ out.push(p);
67
+ }
68
+ this._pairs = out;
69
+ this._notify();
70
+ };
71
+ URLSearchParams.prototype.get = function(name) {
72
+ name = String(name);
73
+ for (var i = 0; i < this._pairs.length; i++) if (this._pairs[i][0] === name) return this._pairs[i][1];
74
+ return null;
75
+ };
76
+ URLSearchParams.prototype.getAll = function(name) {
77
+ name = String(name);
78
+ var out = [];
79
+ for (var i = 0; i < this._pairs.length; i++) if (this._pairs[i][0] === name) out.push(this._pairs[i][1]);
80
+ return out;
81
+ };
82
+ URLSearchParams.prototype.has = function(name, value) {
83
+ name = String(name);
84
+ var hasValue = value !== undefined;
85
+ var v = hasValue ? String(value) : null;
86
+ for (var i = 0; i < this._pairs.length; i++) {
87
+ if (this._pairs[i][0] === name && (!hasValue || this._pairs[i][1] === v)) return true;
88
+ }
89
+ return false;
90
+ };
91
+ URLSearchParams.prototype.set = function(name, value) {
92
+ name = String(name); value = String(value);
93
+ var found = false;
94
+ var out = [];
95
+ for (var i = 0; i < this._pairs.length; i++) {
96
+ var p = this._pairs[i];
97
+ if (p[0] !== name) { out.push(p); continue; }
98
+ if (!found) { out.push([name, value]); found = true; }
99
+ }
100
+ if (!found) out.push([name, value]);
101
+ this._pairs = out;
102
+ this._notify();
103
+ };
104
+ URLSearchParams.prototype.sort = function() {
105
+ this._pairs.sort(function(a, b) { return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0); });
106
+ this._notify();
107
+ };
108
+ URLSearchParams.prototype.forEach = function(cb, thisArg) {
109
+ for (var i = 0; i < this._pairs.length; i++) cb.call(thisArg, this._pairs[i][1], this._pairs[i][0], this);
110
+ };
111
+ URLSearchParams.prototype.keys = function() {
112
+ var out = [];
113
+ for (var i = 0; i < this._pairs.length; i++) out.push(this._pairs[i][0]);
114
+ return out[Symbol.iterator]();
115
+ };
116
+ URLSearchParams.prototype.values = function() {
117
+ var out = [];
118
+ for (var i = 0; i < this._pairs.length; i++) out.push(this._pairs[i][1]);
119
+ return out[Symbol.iterator]();
120
+ };
121
+ URLSearchParams.prototype.entries = function() {
122
+ var out = [];
123
+ for (var i = 0; i < this._pairs.length; i++) out.push(this._pairs[i].slice());
124
+ return out[Symbol.iterator]();
125
+ };
126
+ URLSearchParams.prototype[Symbol.iterator] = function() { return this.entries(); };
127
+ URLSearchParams.prototype.toString = function() {
128
+ var out = [];
129
+ for (var i = 0; i < this._pairs.length; i++) out.push(encodeComponent(this._pairs[i][0]) + '=' + encodeComponent(this._pairs[i][1]));
130
+ return out.join('&');
131
+ };
132
+
133
+ globalThis.URLSearchParams = URLSearchParams;
134
+
135
+ function parseUrlFull(str) {
136
+ str = String(str === undefined || str === null ? '' : str);
137
+ var m = /^([^:\/?#]+:)(?:\/\/(?:([^\/?#@]*)@)?([^\/?#:]*)(?::(\d+))?)?([^?#]*)(\?[^#]*)?(#.*)?$/.exec(str);
138
+ if (!m) return null;
139
+ var protocol = m[1] || '';
140
+ var userinfo = m[2] || '';
141
+ var hostname = m[3] || '';
142
+ var port = m[4] || '';
143
+ var pathname = m[5] || '';
144
+ var search = m[6] || '';
145
+ var hash = m[7] || '';
146
+ if (!pathname && hostname) pathname = '/';
147
+ var username = '', password = '';
148
+ if (userinfo) {
149
+ var idx = userinfo.indexOf(':');
150
+ if (idx === -1) { username = userinfo; } else { username = userinfo.slice(0, idx); password = userinfo.slice(idx + 1); }
151
+ }
152
+ return {
153
+ protocol: protocol, username: username, password: password,
154
+ hostname: hostname, port: port, pathname: pathname, search: search, hash: hash,
155
+ };
156
+ }
157
+
158
+ function serializeUrl(p) {
159
+ var auth = '';
160
+ if (p.username || p.password) auth = p.username + (p.password ? ':' + p.password : '') + '@';
161
+ var host = p.hostname ? ('//' + auth + p.hostname + (p.port ? ':' + p.port : '')) : '';
162
+ return (p.protocol || '') + host + (p.pathname || '') + (p.search || '') + (p.hash || '');
163
+ }
164
+
165
+ function resolveUrlFull(base, next) {
166
+ next = String(next);
167
+ if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(next)) return next;
168
+ var p = parseUrlFull(base) || {};
169
+ var origin = p.hostname
170
+ ? serializeUrl({ protocol: p.protocol, username: p.username, password: p.password, hostname: p.hostname, port: p.port, pathname: '', search: '', hash: '' })
171
+ : (p.protocol || '');
172
+ if (next.charAt(0) === '/') return origin + next;
173
+ if (next.charAt(0) === '#') return base.split('#')[0] + next;
174
+ if (next.charAt(0) === '?') return base.split('?')[0].split('#')[0] + next;
175
+ var dir = (p.pathname || '/').replace(/\/[^\/]*$/, '/');
176
+ return origin + dir + next;
177
+ }
178
+
179
+ function URL(url, base) {
180
+ var href = (base !== undefined && base !== null) ? resolveUrlFull(String(base), url) : String(url);
181
+ if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href)) {
182
+ throw new TypeError('Invalid URL: ' + href);
183
+ }
184
+ this._href = href;
185
+ var self = this;
186
+ var initialSearch = (parseUrlFull(this._href) || {}).search || '';
187
+ this._searchParams = new URLSearchParams(initialSearch);
188
+ this._searchParams._onChange = function() { self._syncSearchFromParams(); };
189
+ }
190
+
191
+ URL.prototype._syncSearchFromParams = function() {
192
+ var s = this._searchParams.toString();
193
+ this._setPart('search', s ? '?' + s : '');
194
+ };
195
+
196
+ URL.prototype._setPart = function(part, value) {
197
+ var p = parseUrlFull(this._href) || {};
198
+ p[part] = value;
199
+ this._href = serializeUrl(p);
200
+ };
201
+
202
+ URL.prototype._resyncSearchParams = function() {
203
+ var s = (parseUrlFull(this._href) || {}).search || '';
204
+ this._searchParams._pairs = parsePairs(s);
205
+ };
206
+
207
+ ['protocol', 'username', 'password', 'hostname', 'port', 'pathname', 'hash'].forEach(function(name) {
208
+ Object.defineProperty(URL.prototype, name, {
209
+ get: function() { return (parseUrlFull(this._href) || {})[name] || ''; },
210
+ set: function(v) { this._setPart(name, String(v)); },
211
+ enumerable: true,
212
+ configurable: true,
213
+ });
214
+ });
215
+
216
+ Object.defineProperty(URL.prototype, 'search', {
217
+ get: function() { return (parseUrlFull(this._href) || {}).search || ''; },
218
+ set: function(v) {
219
+ var s = String(v);
220
+ this._setPart('search', s ? (s.charAt(0) === '?' ? s : '?' + s) : '');
221
+ this._resyncSearchParams();
222
+ },
223
+ enumerable: true,
224
+ configurable: true,
225
+ });
226
+
227
+ Object.defineProperty(URL.prototype, 'searchParams', {
228
+ get: function() { return this._searchParams; },
229
+ enumerable: true,
230
+ configurable: true,
231
+ });
232
+
233
+ Object.defineProperty(URL.prototype, 'host', {
234
+ get: function() {
235
+ var p = parseUrlFull(this._href) || {};
236
+ return p.hostname + (p.port ? ':' + p.port : '');
237
+ },
238
+ set: function(v) {
239
+ var s = String(v);
240
+ var idx = s.indexOf(':');
241
+ if (idx === -1) { this._setPart('hostname', s); this._setPart('port', ''); }
242
+ else { this._setPart('hostname', s.slice(0, idx)); this._setPart('port', s.slice(idx + 1)); }
243
+ },
244
+ enumerable: true,
245
+ configurable: true,
246
+ });
247
+
248
+ Object.defineProperty(URL.prototype, 'origin', {
249
+ get: function() {
250
+ var p = parseUrlFull(this._href) || {};
251
+ if (!p.hostname) return 'null';
252
+ return p.protocol + '//' + p.hostname + (p.port ? ':' + p.port : '');
253
+ },
254
+ enumerable: true,
255
+ configurable: true,
256
+ });
257
+
258
+ Object.defineProperty(URL.prototype, 'href', {
259
+ get: function() { return this._href; },
260
+ set: function(v) {
261
+ this._href = String(v);
262
+ this._resyncSearchParams();
263
+ },
264
+ enumerable: true,
265
+ configurable: true,
266
+ });
267
+
268
+ URL.prototype.toString = function() { return this._href; };
269
+ URL.prototype.toJSON = function() { return this._href; };
270
+
271
+ globalThis.URL = URL;
272
+ })();
273
+ )JS";
274
+
275
+ } // namespace
276
+
277
+ void UrlBindings::install(JSContext* ctx) {
278
+ JSValue result = JS_Eval(ctx, kUrlBootstrapScript, strlen(kUrlBootstrapScript),
279
+ "<url-bootstrap>", JS_EVAL_TYPE_GLOBAL);
280
+ if (JS_IsException(result)) {
281
+ JS_FreeValue(ctx, JS_GetException(ctx));
282
+ }
283
+ JS_FreeValue(ctx, result);
284
+ }
285
+
286
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,12 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ class UrlBindings {
8
+ public:
9
+ static void install(JSContext* ctx);
10
+ };
11
+
12
+ } // namespace margelo::nitro::nitrojsdom
@@ -1,8 +1,10 @@
1
1
  #include "WindowBindings.hpp"
2
+ #include "DOMExceptionBindings.hpp"
2
3
  #include "../DOMBindingsInternal.hpp"
3
4
  #include "../QuickJSRuntime.hpp"
4
5
  #include <cstring>
5
6
  #include <optional>
7
+ #include <random>
6
8
  #include <string>
7
9
  #include <vector>
8
10
 
@@ -10,6 +12,162 @@ namespace margelo::nitro::nitrojsdom {
10
12
 
11
13
  namespace {
12
14
 
15
+ // ── atob / btoa ────────────────────────────────────────────────────────────
16
+
17
+ const char kBase64Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
18
+
19
+ int8_t base64_decode_char(unsigned char c) {
20
+ if (c >= 'A' && c <= 'Z') return c - 'A';
21
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
22
+ if (c >= '0' && c <= '9') return c - '0' + 52;
23
+ if (c == '+') return 62;
24
+ if (c == '/') return 63;
25
+ return -1;
26
+ }
27
+
28
+ bool is_ascii_whitespace(char c) {
29
+ return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
30
+ }
31
+
32
+ JSValue js_window_btoa(JSContext* ctx, JSValue, int argc, JSValue* argv) {
33
+ if (argc < 1) return JS_ThrowTypeError(ctx, "btoa() requires 1 argument");
34
+ JSValue str_val = JS_ToString(ctx, argv[0]);
35
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
36
+ size_t len = 0;
37
+ const char* s = JS_ToCStringLen(ctx, &len, str_val);
38
+ if (!s) { JS_FreeValue(ctx, str_val); return JS_EXCEPTION; }
39
+
40
+ // JS_ToCStringLen yields UTF-8 (length-aware, so embedded NULs survive);
41
+ // btoa operates on Latin1 code units, so decode the UTF-8 back into code
42
+ // points and reject anything outside 0x00-0xFF.
43
+ std::vector<uint8_t> bytes;
44
+ bytes.reserve(len);
45
+ bool out_of_range = false;
46
+ for (size_t i = 0; i < len && !out_of_range;) {
47
+ unsigned char c = static_cast<unsigned char>(s[i]);
48
+ uint32_t codepoint;
49
+ size_t seq_len;
50
+ if (c < 0x80) { codepoint = c; seq_len = 1; }
51
+ else if ((c & 0xE0) == 0xC0 && i + 1 < len) { codepoint = c & 0x1F; seq_len = 2; }
52
+ else if ((c & 0xF0) == 0xE0 && i + 2 < len) { codepoint = c & 0x0F; seq_len = 3; }
53
+ else { out_of_range = true; break; }
54
+ for (size_t k = 1; k < seq_len; k++) {
55
+ codepoint = (codepoint << 6) | (static_cast<unsigned char>(s[i + k]) & 0x3F);
56
+ }
57
+ if (codepoint > 0xFF) { out_of_range = true; break; }
58
+ bytes.push_back(static_cast<uint8_t>(codepoint));
59
+ i += seq_len;
60
+ }
61
+ JS_FreeCString(ctx, s);
62
+ JS_FreeValue(ctx, str_val);
63
+
64
+ if (out_of_range) {
65
+ return throw_dom_exception(ctx, "InvalidCharacterError",
66
+ "The string to be encoded contains characters outside of the Latin1 range.");
67
+ }
68
+
69
+ std::string out;
70
+ out.reserve(((bytes.size() + 2) / 3) * 4);
71
+ size_t i = 0;
72
+ while (i + 3 <= bytes.size()) {
73
+ uint32_t n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
74
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
75
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
76
+ out += kBase64Alphabet[(n >> 6) & 0x3F];
77
+ out += kBase64Alphabet[n & 0x3F];
78
+ i += 3;
79
+ }
80
+ size_t remaining = bytes.size() - i;
81
+ if (remaining == 1) {
82
+ uint32_t n = bytes[i] << 16;
83
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
84
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
85
+ out += "==";
86
+ } else if (remaining == 2) {
87
+ uint32_t n = (bytes[i] << 16) | (bytes[i + 1] << 8);
88
+ out += kBase64Alphabet[(n >> 18) & 0x3F];
89
+ out += kBase64Alphabet[(n >> 12) & 0x3F];
90
+ out += kBase64Alphabet[(n >> 6) & 0x3F];
91
+ out += "=";
92
+ }
93
+
94
+ return JS_NewStringLen(ctx, out.c_str(), out.size());
95
+ }
96
+
97
+ JSValue js_window_atob(JSContext* ctx, JSValue, int argc, JSValue* argv) {
98
+ if (argc < 1) return JS_ThrowTypeError(ctx, "atob() requires 1 argument");
99
+ JSValue str_val = JS_ToString(ctx, argv[0]);
100
+ if (JS_IsException(str_val)) return JS_EXCEPTION;
101
+ size_t len = 0;
102
+ const char* s = JS_ToCStringLen(ctx, &len, str_val);
103
+ if (!s) { JS_FreeValue(ctx, str_val); return JS_EXCEPTION; }
104
+
105
+ std::string data;
106
+ data.reserve(len);
107
+ for (size_t i = 0; i < len; i++) {
108
+ if (!is_ascii_whitespace(s[i])) data += s[i];
109
+ }
110
+ JS_FreeCString(ctx, s);
111
+ JS_FreeValue(ctx, str_val);
112
+
113
+ if (data.size() % 4 == 0) {
114
+ if (data.size() >= 2 && data[data.size() - 1] == '=' && data[data.size() - 2] == '=') {
115
+ data.resize(data.size() - 2);
116
+ } else if (!data.empty() && data.back() == '=') {
117
+ data.resize(data.size() - 1);
118
+ }
119
+ }
120
+
121
+ if (data.size() % 4 == 1) {
122
+ return throw_dom_exception(ctx, "InvalidCharacterError",
123
+ "The string to be decoded is not correctly encoded.");
124
+ }
125
+ for (char c : data) {
126
+ if (base64_decode_char(static_cast<unsigned char>(c)) < 0) {
127
+ return throw_dom_exception(ctx, "InvalidCharacterError",
128
+ "The string to be decoded is not correctly encoded.");
129
+ }
130
+ }
131
+
132
+ std::string out;
133
+ out.reserve((data.size() / 4) * 3 + 3);
134
+ size_t i = 0;
135
+ while (i + 4 <= data.size()) {
136
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12) |
137
+ (base64_decode_char(data[i + 2]) << 6) | base64_decode_char(data[i + 3]);
138
+ out += static_cast<char>((n >> 16) & 0xFF);
139
+ out += static_cast<char>((n >> 8) & 0xFF);
140
+ out += static_cast<char>(n & 0xFF);
141
+ i += 4;
142
+ }
143
+ size_t remaining = data.size() - i;
144
+ if (remaining == 2) {
145
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12);
146
+ out += static_cast<char>((n >> 16) & 0xFF);
147
+ } else if (remaining == 3) {
148
+ uint32_t n = (base64_decode_char(data[i]) << 18) | (base64_decode_char(data[i + 1]) << 12) |
149
+ (base64_decode_char(data[i + 2]) << 6);
150
+ out += static_cast<char>((n >> 16) & 0xFF);
151
+ out += static_cast<char>((n >> 8) & 0xFF);
152
+ }
153
+
154
+ // atob's result is a "binary string" — each output char code is one decoded
155
+ // byte (0x00-0xFF). JS_NewStringLen expects UTF-8 input, so each byte must
156
+ // be re-encoded as the UTF-8 sequence for that same code point (1 byte for
157
+ // 0x00-0x7F, 2 bytes for 0x80-0xFF) to round-trip through QuickJS correctly.
158
+ std::string utf8;
159
+ utf8.reserve(out.size() * 2);
160
+ for (unsigned char byte : out) {
161
+ if (byte < 0x80) {
162
+ utf8 += static_cast<char>(byte);
163
+ } else {
164
+ utf8 += static_cast<char>(0xC0 | (byte >> 6));
165
+ utf8 += static_cast<char>(0x80 | (byte & 0x3F));
166
+ }
167
+ }
168
+ return JS_NewStringLen(ctx, utf8.c_str(), utf8.size());
169
+ }
170
+
13
171
  // ── console ────────────────────────────────────────────────────────────────
14
172
 
15
173
  JSValue js_console_method(JSContext* ctx, JSValue, int argc, JSValue* argv, int magic) {
@@ -181,6 +339,94 @@ const char* kLocationBootstrapScript = R"JS(
181
339
  })();
182
340
  )JS";
183
341
 
342
+ // ── crypto.getRandomValues ────────────────────────────────────────────────────
343
+
344
+ JSValue js_native_random_bytes(JSContext* ctx, JSValue, int argc, JSValue* argv) {
345
+ int32_t count = 0;
346
+ if (argc >= 1) JS_ToInt32(ctx, &count, argv[0]);
347
+ if (count < 0) count = 0;
348
+
349
+ static thread_local std::mt19937 gen{std::random_device{}()};
350
+ std::uniform_int_distribution<int> dist(0, 255);
351
+
352
+ JSValue arr = JS_NewArray(ctx);
353
+ for (int32_t i = 0; i < count; i++) {
354
+ JS_SetPropertyUint32(ctx, arr, static_cast<uint32_t>(i), JS_NewInt32(ctx, dist(gen)));
355
+ }
356
+ return arr;
357
+ }
358
+
359
+ const char* kCryptoBootstrapScript = R"JS(
360
+ (function() {
361
+ globalThis.crypto = globalThis.crypto || {};
362
+ globalThis.crypto.getRandomValues = function(typedArray) {
363
+ if (!typedArray || typeof typedArray.byteLength !== 'number' || typedArray instanceof BigInt64Array || typedArray instanceof BigUint64Array) {
364
+ throw new TypeError('crypto.getRandomValues() requires an integer TypedArray argument');
365
+ }
366
+ if (typedArray.byteLength > 65536) {
367
+ var err = new Error('crypto.getRandomValues() can only fill up to 65536 bytes at a time');
368
+ err.name = 'QuotaExceededError';
369
+ throw err;
370
+ }
371
+ var bytes = __nativeRandomBytes(typedArray.byteLength);
372
+ var view = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
373
+ for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
374
+ return typedArray;
375
+ };
376
+ })();
377
+ )JS";
378
+
379
+ // ── navigator ──────────────────────────────────────────────────────────────
380
+
381
+ const char* kNavigatorBootstrapScript = R"JS(
382
+ (function() {
383
+ var navigator = {
384
+ userAgent: 'Mozilla/5.0 (ReactNative) react-native-nitro-jsdom',
385
+ platform: 'ReactNative',
386
+ language: 'en-US',
387
+ languages: ['en-US'],
388
+ onLine: true,
389
+ cookieEnabled: false,
390
+ hardwareConcurrency: 1,
391
+ };
392
+ globalThis.navigator = navigator;
393
+ })();
394
+ )JS";
395
+
396
+ // ── matchMedia ─────────────────────────────────────────────────────────────
397
+ // No layout engine backs this sandbox, so every query reports "no match" —
398
+ // mirrors jsdom's own stance that layout-dependent APIs return inert defaults
399
+ // rather than throwing.
400
+
401
+ const char* kMatchMediaBootstrapScript = R"JS(
402
+ (function() {
403
+ function MediaQueryList(media) {
404
+ this.media = String(media === undefined ? '' : media);
405
+ this.matches = false;
406
+ this.onchange = null;
407
+ this._listeners = [];
408
+ }
409
+ MediaQueryList.prototype.addListener = function(cb) {
410
+ if (typeof cb === 'function' && this._listeners.indexOf(cb) === -1) this._listeners.push(cb);
411
+ };
412
+ MediaQueryList.prototype.removeListener = function(cb) {
413
+ var idx = this._listeners.indexOf(cb);
414
+ if (idx !== -1) this._listeners.splice(idx, 1);
415
+ };
416
+ MediaQueryList.prototype.addEventListener = function(type, cb) {
417
+ if (type === 'change') this.addListener(cb);
418
+ };
419
+ MediaQueryList.prototype.removeEventListener = function(type, cb) {
420
+ if (type === 'change') this.removeListener(cb);
421
+ };
422
+ MediaQueryList.prototype.dispatchEvent = function() { return true; };
423
+
424
+ globalThis.matchMedia = function(query) {
425
+ return new MediaQueryList(query);
426
+ };
427
+ })();
428
+ )JS";
429
+
184
430
  } // namespace
185
431
 
186
432
  void WindowBindings::install(JSContext* ctx) {
@@ -189,6 +435,8 @@ void WindowBindings::install(JSContext* ctx) {
189
435
  JS_SetPropertyStr(ctx, global, "alert", JS_NewCFunction(ctx, js_window_alert, "alert", 1));
190
436
  JS_SetPropertyStr(ctx, global, "confirm", JS_NewCFunction(ctx, js_window_confirm, "confirm", 1));
191
437
  JS_SetPropertyStr(ctx, global, "prompt", JS_NewCFunction(ctx, js_window_prompt, "prompt", 2));
438
+ JS_SetPropertyStr(ctx, global, "atob", JS_NewCFunction(ctx, js_window_atob, "atob", 1));
439
+ JS_SetPropertyStr(ctx, global, "btoa", JS_NewCFunction(ctx, js_window_btoa, "btoa", 1));
192
440
 
193
441
  JSValue console_obj = JS_NewObject(ctx);
194
442
  JS_SetPropertyStr(ctx, console_obj, "log", JS_NewCFunctionMagic(ctx, js_console_method, "log", 0, JS_CFUNC_generic_magic, 0));
@@ -197,6 +445,7 @@ void WindowBindings::install(JSContext* ctx) {
197
445
  JS_SetPropertyStr(ctx, console_obj, "info", JS_NewCFunctionMagic(ctx, js_console_method, "info", 0, JS_CFUNC_generic_magic, 3));
198
446
  JS_SetPropertyStr(ctx, console_obj, "debug", JS_NewCFunctionMagic(ctx, js_console_method, "debug", 0, JS_CFUNC_generic_magic, 4));
199
447
  JS_SetPropertyStr(ctx, global, "console", console_obj);
448
+ JS_SetPropertyStr(ctx, global, "__nativeRandomBytes", JS_NewCFunction(ctx, js_native_random_bytes, "__nativeRandomBytes", 1));
200
449
 
201
450
  JS_FreeValue(ctx, global);
202
451
 
@@ -206,6 +455,27 @@ void WindowBindings::install(JSContext* ctx) {
206
455
  JS_FreeValue(ctx, JS_GetException(ctx));
207
456
  }
208
457
  JS_FreeValue(ctx, location_result);
458
+
459
+ JSValue crypto_result = JS_Eval(ctx, kCryptoBootstrapScript, strlen(kCryptoBootstrapScript),
460
+ "<crypto-bootstrap>", JS_EVAL_TYPE_GLOBAL);
461
+ if (JS_IsException(crypto_result)) {
462
+ JS_FreeValue(ctx, JS_GetException(ctx));
463
+ }
464
+ JS_FreeValue(ctx, crypto_result);
465
+
466
+ JSValue navigator_result = JS_Eval(ctx, kNavigatorBootstrapScript, strlen(kNavigatorBootstrapScript),
467
+ "<navigator-bootstrap>", JS_EVAL_TYPE_GLOBAL);
468
+ if (JS_IsException(navigator_result)) {
469
+ JS_FreeValue(ctx, JS_GetException(ctx));
470
+ }
471
+ JS_FreeValue(ctx, navigator_result);
472
+
473
+ JSValue match_media_result = JS_Eval(ctx, kMatchMediaBootstrapScript, strlen(kMatchMediaBootstrapScript),
474
+ "<match-media-bootstrap>", JS_EVAL_TYPE_GLOBAL);
475
+ if (JS_IsException(match_media_result)) {
476
+ JS_FreeValue(ctx, JS_GetException(ctx));
477
+ }
478
+ JS_FreeValue(ctx, match_media_result);
209
479
  }
210
480
 
211
481
  } // namespace margelo::nitro::nitrojsdom
@@ -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));