@salve-software/react-native-nitro-jsdom 1.0.1 → 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.
- package/android/CMakeLists.txt +14 -0
- package/cpp/HybridHtmlSandbox.cpp +16 -3
- package/cpp/HybridHtmlSandbox.hpp +1 -1
- package/cpp/lexbor/LexborDocument.cpp +217 -67
- package/cpp/lexbor/LexborDocument.hpp +29 -10
- package/cpp/quickjs/DOMBindings.cpp +28 -0
- package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
- package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
- package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
- package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
- package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
- package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
- package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
- package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
- package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
- package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
- package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
- package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
- package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
- package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
- package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
- package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
- package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
- package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
- package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
- package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
- package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
- package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
- package/package.json +1 -1
- package/src/classes/JSDOM/JSDOM.class.ts +1 -0
- 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
|
|
@@ -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));
|