@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.
Files changed (56) hide show
  1. package/android/CMakeLists.txt +14 -0
  2. package/cpp/HybridHtmlSandbox.cpp +16 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +217 -67
  5. package/cpp/lexbor/LexborDocument.hpp +29 -10
  6. package/cpp/quickjs/DOMBindings.cpp +28 -0
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -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/DocumentBindings.cpp +84 -4
  25. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  26. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  27. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  28. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  29. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  30. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  32. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  34. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  35. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  36. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  38. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  40. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  41. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  42. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  43. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  44. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  46. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  48. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  50. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  51. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  53. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  54. package/package.json +1 -1
  55. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  56. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -0,0 +1,118 @@
1
+ #include "CookieBindings.hpp"
2
+ #include "../DOMBindingsInternal.hpp"
3
+ #include "../QuickJSRuntime.hpp"
4
+ #include "../Storage.hpp"
5
+ #include <string>
6
+ #include <cctype>
7
+ #include <cstdlib>
8
+ #include <ctime>
9
+
10
+ namespace margelo::nitro::nitrojsdom {
11
+
12
+ namespace {
13
+
14
+ std::string trim(const std::string& s) {
15
+ size_t start = s.find_first_not_of(" \t\n\r");
16
+ if (start == std::string::npos) return "";
17
+ size_t end = s.find_last_not_of(" \t\n\r");
18
+ return s.substr(start, end - start + 1);
19
+ }
20
+
21
+ std::string to_lower(std::string s) {
22
+ for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
23
+ return s;
24
+ }
25
+
26
+ // Best-effort: accepts the date formats real cookie strings actually use
27
+ // (RFC 1123, RFC 850, asctime) — not a full HTTP-date parser.
28
+ bool is_past_date(const std::string& value) {
29
+ static const char* kFormats[] = {
30
+ "%a, %d %b %Y %H:%M:%S", // RFC 1123: "Thu, 01 Jan 1970 00:00:00 GMT"
31
+ "%A, %d-%b-%y %H:%M:%S", // RFC 850
32
+ "%a %b %d %H:%M:%S %Y", // asctime
33
+ };
34
+ for (const char* fmt : kFormats) {
35
+ struct tm parsed{};
36
+ if (strptime(value.c_str(), fmt, &parsed)) {
37
+ return timegm(&parsed) <= time(nullptr);
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ // A cookie write is a deletion when `max-age` is <= 0 or `expires` names a
44
+ // date in the past — the two idioms real-world scripts use to clear a
45
+ // cookie (most commonly `expires=Thu, 01 Jan 1970 00:00:00 GMT`).
46
+ bool is_deletion(const std::string& attrs) {
47
+ size_t pos = 0;
48
+ while (pos < attrs.size()) {
49
+ size_t semi = attrs.find(';', pos);
50
+ std::string attr = attrs.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos);
51
+ pos = semi == std::string::npos ? attrs.size() : semi + 1;
52
+
53
+ size_t eq = attr.find('=');
54
+ if (eq == std::string::npos) continue;
55
+ std::string key = to_lower(trim(attr.substr(0, eq)));
56
+ std::string value = trim(attr.substr(eq + 1));
57
+
58
+ if (key == "max-age") {
59
+ if (std::atol(value.c_str()) <= 0) return true;
60
+ } else if (key == "expires") {
61
+ if (is_past_date(value)) return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ JSValue js_doc_get_cookie(JSContext* ctx, JSValue) {
68
+ auto* rctx = get_ctx(ctx);
69
+ if (!rctx) return JS_NewString(ctx, "");
70
+
71
+ std::string result;
72
+ for (size_t i = 0; i < rctx->cookie_jar.length(); i++) {
73
+ auto key = rctx->cookie_jar.key(i);
74
+ if (!key) continue;
75
+ auto value = rctx->cookie_jar.getItem(*key);
76
+ if (!value) continue;
77
+ if (!result.empty()) result += "; ";
78
+ result += *key + "=" + *value;
79
+ }
80
+ return JS_NewStringLen(ctx, result.data(), result.size());
81
+ }
82
+
83
+ JSValue js_doc_set_cookie(JSContext* ctx, JSValue, JSValue val) {
84
+ auto* rctx = get_ctx(ctx);
85
+ const char* str = JS_ToCString(ctx, val);
86
+ if (str && rctx) {
87
+ std::string input(str);
88
+ size_t semi = input.find(';');
89
+ std::string pair = semi == std::string::npos ? input : input.substr(0, semi);
90
+ std::string attrs = semi == std::string::npos ? "" : input.substr(semi + 1);
91
+ size_t eq = pair.find('=');
92
+ if (eq != std::string::npos) {
93
+ std::string name = trim(pair.substr(0, eq));
94
+ std::string value = trim(pair.substr(eq + 1));
95
+ if (!name.empty()) {
96
+ if (is_deletion(attrs)) {
97
+ rctx->cookie_jar.removeItem(name);
98
+ } else {
99
+ rctx->cookie_jar.setItem(name, value);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ if (str) JS_FreeCString(ctx, str);
105
+ return JS_UNDEFINED;
106
+ }
107
+
108
+ } // namespace
109
+
110
+ void CookieBindings::install(JSContext* ctx) {
111
+ JSValue global = JS_GetGlobalObject(ctx);
112
+ JSValue doc = JS_GetPropertyStr(ctx, global, "document");
113
+ define_prop(ctx, doc, "cookie", js_doc_get_cookie, js_doc_set_cookie);
114
+ JS_FreeValue(ctx, doc);
115
+ JS_FreeValue(ctx, global);
116
+ }
117
+
118
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,23 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers document.cookie (get/set), backed by an in-memory per-runtime
8
+ // cookie jar (RuntimeContext::cookie_jar — a name -> value map preserving
9
+ // insertion order). jsdom backs this with a real tough-cookie CookieJar
10
+ // scoped by domain/path/expiry; this sandbox has no navigation or multiple
11
+ // origins, so scoping attributes (path/domain/secure/samesite) are parsed
12
+ // off and discarded rather than enforced. `expires`/`max-age` are the
13
+ // exception: a value in the past (or max-age <= 0) is treated as the
14
+ // deletion idiom real-world scripts use it for, and removes the cookie
15
+ // instead of leaving a stray "name=" entry in the jar. `expires` parsing is
16
+ // best-effort (RFC 1123/850/asctime formats only, not a full HTTP-date parser).
17
+ //
18
+ // Must run after DocumentBindings (needs globalThis.document to exist).
19
+ struct CookieBindings {
20
+ static void install(JSContext* ctx);
21
+ };
22
+
23
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,341 @@
1
+ #include "CustomElementsBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kCustomElementsBootstrapScript = R"JS(
9
+ (function() {
10
+ var RESERVED_NAMES = [
11
+ 'annotation-xml', 'color-profile', 'font-face', 'font-face-src',
12
+ 'font-face-uri', 'font-face-format', 'font-face-name', 'missing-glyph',
13
+ ];
14
+ var NAME_RE = /^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/;
15
+
16
+ function isValidName(name) {
17
+ if (typeof name !== 'string' || name === '') return false;
18
+ if (RESERVED_NAMES.indexOf(name) !== -1) return false;
19
+ return NAME_RE.test(name);
20
+ }
21
+
22
+ // Elements/ShadowRoots don't naturally track their own document, so
23
+ // connectivity is derived by walking parentNode (crossing shadow
24
+ // boundaries via .host) until documentElement is reached.
25
+ function isConnected(node) {
26
+ var docEl = document.documentElement;
27
+ var n = node;
28
+ while (n) {
29
+ if (n === docEl) return true;
30
+ n = ('host' in n) ? n.host : n.parentNode;
31
+ }
32
+ return false;
33
+ }
34
+
35
+ // No global registry of upgraded elements: every hook below scopes itself
36
+ // to the subtree it actually touched (via a local, naturally
37
+ // garbage-collectible JS array), so nothing here can leak memory or grow
38
+ // unbounded, and no hook costs more than the size of the DOM it mutated.
39
+
40
+ function fireConnectivityChange(el, connected) {
41
+ el.__ceConnected = connected;
42
+ if (connected) {
43
+ if (typeof el.connectedCallback === 'function') {
44
+ try { el.connectedCallback(); } catch (e) {}
45
+ }
46
+ } else {
47
+ if (typeof el.disconnectedCallback === 'function') {
48
+ try { el.disconnectedCallback(); } catch (e) {}
49
+ }
50
+ }
51
+ }
52
+
53
+ function upgradeElement(el) {
54
+ if (!el || el.__ceUpgraded) return;
55
+ var tag = el.tagName ? el.tagName.toLowerCase() : null;
56
+ if (!tag) return;
57
+ var def = customElements._defs[tag];
58
+ if (!def) return;
59
+
60
+ Object.setPrototypeOf(el, def.ctor.prototype);
61
+ Object.defineProperty(el, '__ceUpgraded', { value: true, configurable: true });
62
+ Object.defineProperty(el, '__ceConnected', { value: false, writable: true, configurable: true });
63
+
64
+ def.observedAttributes.forEach(function(attr) {
65
+ if (el.hasAttribute(attr) && typeof el.attributeChangedCallback === 'function') {
66
+ try { el.attributeChangedCallback(attr, null, el.getAttribute(attr), null); } catch (e) {}
67
+ }
68
+ });
69
+
70
+ if (isConnected(el)) fireConnectivityChange(el, true);
71
+ }
72
+
73
+ function upgradeAllMatching(name) {
74
+ var found = document.querySelectorAll(name);
75
+ for (var i = 0; i < found.length; i++) upgradeElement(found[i]);
76
+ }
77
+
78
+ // Returns the shadow root attached to `el`, if any, regardless of mode.
79
+ // Element.prototype.shadowRoot hides closed-mode roots from user script by
80
+ // spec, but our own upgrade/connectivity bookkeeping isn't user script —
81
+ // it needs to see into every shadow tree the same way the UA internally
82
+ // does. Populated by the attachShadow() patch further down.
83
+ function shadowRootOf(el) {
84
+ return el ? el.__ceShadowRoot : null;
85
+ }
86
+
87
+ // Collects every descendant of `root` (light DOM), recursing into root's
88
+ // OWN attached shadow root (a custom element's shadow content isn't a
89
+ // light-DOM child of anything — querySelectorAll('*') alone would never
90
+ // find it) as well as each descendant's shadow root, so custom elements
91
+ // living inside shadow trees aren't invisible to the subtree-scoped hooks
92
+ // below. Does not include `root` itself.
93
+ function collectDescendantsIncludingShadow(root, out) {
94
+ if (!root) return;
95
+ var rootSr = shadowRootOf(root);
96
+ if (rootSr) collectDescendantsIncludingShadow(rootSr, out);
97
+ if (typeof root.querySelectorAll !== 'function') return;
98
+ var found = root.querySelectorAll('*');
99
+ for (var i = 0; i < found.length; i++) {
100
+ var el = found[i];
101
+ out.push(el);
102
+ var sr = shadowRootOf(el);
103
+ if (sr) collectDescendantsIncludingShadow(sr, out);
104
+ }
105
+ }
106
+
107
+ function upgradeSubtree(root) {
108
+ var found = [];
109
+ collectDescendantsIncludingShadow(root, found);
110
+ for (var i = 0; i < found.length; i++) upgradeElement(found[i]);
111
+ }
112
+
113
+ // Fires disconnectedCallback for every already-upgraded, already-connected
114
+ // custom element currently in root's subtree (light DOM + nested shadow
115
+ // trees). Call this on the OLD content of a node right BEFORE it's
116
+ // wholesale-replaced (innerHTML assignment unconditionally destroys every
117
+ // existing child), while it's still walkable.
118
+ function disconnectSubtree(root) {
119
+ var found = [];
120
+ collectDescendantsIncludingShadow(root, found);
121
+ for (var i = 0; i < found.length; i++) {
122
+ var el = found[i];
123
+ if (el.__ceUpgraded && el.__ceConnected) fireConnectivityChange(el, false);
124
+ }
125
+ }
126
+
127
+ // Re-checks connectivity of `node`, and every descendant across light DOM
128
+ // and nested shadow trees, firing connectedCallback/disconnectedCallback on
129
+ // transitions. Used by appendChild/removeChild, which know exactly which
130
+ // subtree moved, so this stays O(size of that subtree) regardless of how
131
+ // many custom elements exist elsewhere in the document.
132
+ function syncConnectedSubtree(node) {
133
+ if (!node) return;
134
+ if (node.__ceUpgraded) {
135
+ var connected = isConnected(node);
136
+ if (connected !== node.__ceConnected) fireConnectivityChange(node, connected);
137
+ }
138
+ var found = [];
139
+ collectDescendantsIncludingShadow(node, found);
140
+ for (var i = 0; i < found.length; i++) {
141
+ var el = found[i];
142
+ if (!el.__ceUpgraded) continue;
143
+ var elConnected = isConnected(el);
144
+ if (elConnected !== el.__ceConnected) fireConnectivityChange(el, elConnected);
145
+ }
146
+ }
147
+
148
+ // ── CustomElementRegistry ────────────────────────────────────────────────
149
+
150
+ function CustomElementRegistry() {
151
+ this._defs = Object.create(null);
152
+ this._pending = Object.create(null);
153
+ this._ctors = [];
154
+ }
155
+
156
+ CustomElementRegistry.prototype.define = function(name, ctor, options) {
157
+ if (typeof ctor !== 'function') {
158
+ throw new TypeError("Failed to execute 'define' on 'CustomElementRegistry': " +
159
+ "the constructor must be a function");
160
+ }
161
+ if (!isValidName(name)) {
162
+ throw new DOMException("'" + name + "' is not a valid custom element name", 'SyntaxError');
163
+ }
164
+ if (this._defs[name]) {
165
+ throw new DOMException(
166
+ "the name \"" + name + "\" has already been used with this registry", 'NotSupportedError');
167
+ }
168
+ if (this._ctors.indexOf(ctor) !== -1) {
169
+ throw new DOMException(
170
+ 'this constructor has already been used with this registry', 'NotSupportedError');
171
+ }
172
+
173
+ var observed = ctor.observedAttributes;
174
+ this._defs[name] = {
175
+ ctor: ctor,
176
+ observedAttributes: Array.isArray(observed) ? observed : [],
177
+ };
178
+ this._ctors.push(ctor);
179
+
180
+ upgradeAllMatching(name);
181
+
182
+ if (this._pending[name]) {
183
+ this._pending[name].resolve();
184
+ delete this._pending[name];
185
+ }
186
+ };
187
+
188
+ CustomElementRegistry.prototype.get = function(name) {
189
+ var def = this._defs[name];
190
+ return def ? def.ctor : undefined;
191
+ };
192
+
193
+ CustomElementRegistry.prototype.whenDefined = function(name) {
194
+ if (!isValidName(name)) {
195
+ return Promise.reject(new DOMException("'" + name + "' is not a valid custom element name", 'SyntaxError'));
196
+ }
197
+ if (this._defs[name]) return Promise.resolve();
198
+ if (!this._pending[name]) {
199
+ var resolveFn;
200
+ var promise = new Promise(function(resolve) { resolveFn = resolve; });
201
+ this._pending[name] = { promise: promise, resolve: resolveFn };
202
+ }
203
+ return this._pending[name].promise;
204
+ };
205
+
206
+ // upgrade(root): shadow-including INCLUSIVE descendants — root itself (if
207
+ // it's an element) plus everything upgradeSubtree already walks (which is
208
+ // exclusive of root, since its other callers — innerHTML/insertAdjacentHTML
209
+ // patches below — always pass a pre-existing container that was never
210
+ // itself a fresh upgrade candidate).
211
+ CustomElementRegistry.prototype.upgrade = function(root) {
212
+ if (!(root instanceof Node)) {
213
+ throw new TypeError(
214
+ "Failed to execute 'upgrade' on 'CustomElementRegistry': parameter 1 is not of type 'Node'");
215
+ }
216
+ var ELEMENT_NODE = 1;
217
+ if (root.nodeType === ELEMENT_NODE) upgradeElement(root);
218
+ upgradeSubtree(root);
219
+ };
220
+
221
+ globalThis.CustomElementRegistry = CustomElementRegistry;
222
+ globalThis.customElements = new CustomElementRegistry();
223
+
224
+ // ── Upgrade hooks: document.createElement ────────────────────────────────
225
+
226
+ var origCreateElement = document.createElement;
227
+ document.createElement = function(tag) {
228
+ var el = origCreateElement.call(document, tag);
229
+ upgradeElement(el);
230
+ return el;
231
+ };
232
+
233
+ // ── Upgrade hooks: Element.prototype.innerHTML / ShadowRoot.prototype.innerHTML ──
234
+
235
+ function patchInnerHTML(proto) {
236
+ var desc = Object.getOwnPropertyDescriptor(proto, 'innerHTML');
237
+ if (!desc || !desc.set) return;
238
+ var origSet = desc.set;
239
+ Object.defineProperty(proto, 'innerHTML', {
240
+ get: desc.get,
241
+ set: function(html) {
242
+ disconnectSubtree(this); // walk the OLD content before it's replaced
243
+ origSet.call(this, html);
244
+ upgradeSubtree(this); // upgrade/connect the NEW content
245
+ },
246
+ enumerable: desc.enumerable,
247
+ configurable: true,
248
+ });
249
+ }
250
+ patchInnerHTML(Element.prototype);
251
+ patchInnerHTML(ShadowRoot.prototype);
252
+
253
+ // ── Track attached shadow roots for internal (mode-agnostic) traversal ───
254
+
255
+ var origAttachShadow = Element.prototype.attachShadow;
256
+ Element.prototype.attachShadow = function(options) {
257
+ var root = origAttachShadow.call(this, options);
258
+ Object.defineProperty(this, '__ceShadowRoot', { value: root, configurable: true });
259
+ return root;
260
+ };
261
+
262
+ // ── Upgrade hooks: Element.prototype.insertAdjacentHTML ──────────────────
263
+
264
+ // insertAdjacentHTML only ever adds content, never removes any — no
265
+ // existing element can be disconnected by it, so there's nothing to sync
266
+ // beyond upgrading (and, via upgradeElement, connecting) what's new.
267
+ var origInsertAdjacentHTML = Element.prototype.insertAdjacentHTML;
268
+ Element.prototype.insertAdjacentHTML = function(position, html) {
269
+ origInsertAdjacentHTML.call(this, position, html);
270
+ upgradeSubtree(this);
271
+ var pos = String(position).toLowerCase();
272
+ if ((pos === 'beforebegin' || pos === 'afterend') && this.parentNode) {
273
+ upgradeSubtree(this.parentNode);
274
+ }
275
+ };
276
+
277
+ // ── Connect/disconnect hooks: Node.prototype.appendChild / removeChild ───
278
+
279
+ var DOCUMENT_FRAGMENT_NODE = 11;
280
+
281
+ var origAppendChild = Node.prototype.appendChild;
282
+ Node.prototype.appendChild = function(child) {
283
+ var moved = (child && child.nodeType === DOCUMENT_FRAGMENT_NODE)
284
+ ? Array.prototype.slice.call(child.childNodes)
285
+ : [child];
286
+ var result = origAppendChild.call(this, child);
287
+ moved.forEach(syncConnectedSubtree);
288
+ return result;
289
+ };
290
+
291
+ var origRemoveChild = Node.prototype.removeChild;
292
+ Node.prototype.removeChild = function(child) {
293
+ var result = origRemoveChild.call(this, child);
294
+ syncConnectedSubtree(child);
295
+ return result;
296
+ };
297
+
298
+ // ── attributeChangedCallback hooks: setAttribute / removeAttribute ───────
299
+
300
+ function observedDefFor(el) {
301
+ if (!el.__ceUpgraded) return null;
302
+ var tag = el.tagName ? el.tagName.toLowerCase() : null;
303
+ return tag ? customElements._defs[tag] : null;
304
+ }
305
+
306
+ var origSetAttribute = Element.prototype.setAttribute;
307
+ Element.prototype.setAttribute = function(name, value) {
308
+ var def = observedDefFor(this);
309
+ var oldValue = def ? this.getAttribute(name) : null;
310
+ origSetAttribute.call(this, name, value);
311
+ if (def && def.observedAttributes.indexOf(name) !== -1 &&
312
+ typeof this.attributeChangedCallback === 'function') {
313
+ try { this.attributeChangedCallback(name, oldValue, this.getAttribute(name), null); } catch (e) {}
314
+ }
315
+ };
316
+
317
+ var origRemoveAttribute = Element.prototype.removeAttribute;
318
+ Element.prototype.removeAttribute = function(name) {
319
+ var def = observedDefFor(this);
320
+ var oldValue = def ? this.getAttribute(name) : null;
321
+ origRemoveAttribute.call(this, name);
322
+ if (def && def.observedAttributes.indexOf(name) !== -1 &&
323
+ typeof this.attributeChangedCallback === 'function') {
324
+ try { this.attributeChangedCallback(name, oldValue, null, null); } catch (e) {}
325
+ }
326
+ };
327
+ })();
328
+ )JS";
329
+
330
+ } // namespace
331
+
332
+ void CustomElementsBindings::install(JSContext* ctx) {
333
+ JSValue result = JS_Eval(ctx, kCustomElementsBootstrapScript, strlen(kCustomElementsBootstrapScript),
334
+ "<custom-elements-bootstrap>", JS_EVAL_TYPE_GLOBAL);
335
+ if (JS_IsException(result)) {
336
+ JS_FreeValue(ctx, JS_GetException(ctx));
337
+ }
338
+ JS_FreeValue(ctx, result);
339
+ }
340
+
341
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,50 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers globalThis.customElements (CustomElementRegistry: define/get/
8
+ // whenDefined/upgrade) plus upgrade hooks. Pure JS, built entirely on top of
9
+ // already-exposed primitives (querySelectorAll, createElement, innerHTML,
10
+ // appendChild, removeChild, setAttribute/removeAttribute, attachShadow) via
11
+ // monkey-patching — no new native bindings.
12
+ //
13
+ // Scope/limitations (documented rather than silently wrong):
14
+ // - Elements are "upgraded" by reassigning their prototype to the registered
15
+ // class's prototype (Object.setPrototypeOf) rather than by invoking the
16
+ // class's constructor with the element as `this`. Real custom element
17
+ // upgrade requires cooperation from the JS engine's class-construction
18
+ // protocol (a real `new.target`-aware HTMLElement constructor) that this
19
+ // sandbox doesn't implement, so constructor logic never runs — only
20
+ // connectedCallback/disconnectedCallback/attributeChangedCallback do.
21
+ // - Upgrade triggers: document.createElement(), a customElements.define()
22
+ // call rescanning the current document (covers elements already present
23
+ // from the initial HTML), Element/ShadowRoot.innerHTML assignment, and
24
+ // insertAdjacentHTML. Other insertion paths (before/after/replaceWith/
25
+ // append/prepend) are not hooked.
26
+ // - The define()-time rescan walks document.querySelectorAll(name) only, so
27
+ // it does not cross shadow boundaries: elements already sitting inside an
28
+ // existing shadow root are not retroactively upgraded when a matching tag
29
+ // is define()'d afterward. (Populating that shadow root's innerHTML after
30
+ // the define() call still upgrades them, per the innerHTML trigger above.)
31
+ // Calling customElements.upgrade(root) manually on that shadow root (or any
32
+ // ancestor whose shadow-including subtree contains it) covers this case.
33
+ // - customElements.upgrade(root) walks root's shadow-including INCLUSIVE
34
+ // descendants (root itself, if an element, plus everything reachable via
35
+ // the same light-DOM + shadow traversal the other hooks use). Throws
36
+ // TypeError if root isn't a Node (matches the spec: null/undefined/plain
37
+ // objects are rejected, not silently no-op'd).
38
+ // - attachShadow() is wrapped purely to tag the host element with an
39
+ // internal, mode-agnostic reference to its shadow root (bypassing the
40
+ // closed-mode privacy Element.prototype.shadowRoot enforces for user
41
+ // script), so appendChild/removeChild/innerHTML connectivity bookkeeping
42
+ // can descend into shadow trees the same way it walks light-DOM children.
43
+ //
44
+ // Must run after ElementBindings, DocumentBindings, and ShadowRootBindings
45
+ // (wraps their innerHTML/createElement/appendChild/removeChild/attachShadow).
46
+ struct CustomElementsBindings {
47
+ static void install(JSContext* ctx);
48
+ };
49
+
50
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,111 @@
1
+ #include "DOMExceptionBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kDOMExceptionBootstrapScript = R"JS(
9
+ (function() {
10
+ var LEGACY_CODES = {
11
+ IndexSizeError: 1,
12
+ HierarchyRequestError: 3,
13
+ WrongDocumentError: 4,
14
+ InvalidCharacterError: 5,
15
+ NoModificationAllowedError: 7,
16
+ NotFoundError: 8,
17
+ NotSupportedError: 9,
18
+ InUseAttributeError: 10,
19
+ InvalidStateError: 11,
20
+ SyntaxError: 12,
21
+ InvalidModificationError: 13,
22
+ NamespaceError: 14,
23
+ InvalidAccessError: 15,
24
+ TypeMismatchError: 17,
25
+ SecurityError: 18,
26
+ NetworkError: 19,
27
+ AbortError: 20,
28
+ URLMismatchError: 21,
29
+ QuotaExceededError: 22,
30
+ TimeoutError: 23,
31
+ InvalidNodeTypeError: 24,
32
+ DataCloneError: 25,
33
+ };
34
+
35
+ function DOMException(message, name) {
36
+ this.message = message !== undefined ? String(message) : '';
37
+ this.name = name !== undefined ? String(name) : 'Error';
38
+ this.code = LEGACY_CODES[this.name] || 0;
39
+ this.stack = new Error(this.message).stack;
40
+ }
41
+ DOMException.prototype = Object.create(Error.prototype);
42
+ DOMException.prototype.constructor = DOMException;
43
+ DOMException.prototype.toString = function() {
44
+ return this.name + ': ' + this.message;
45
+ };
46
+
47
+ // Legacy static/instance numeric constants, e.g. DOMException.NOT_FOUND_ERR.
48
+ var STATIC_CONST_NAMES = {
49
+ IndexSizeError: 'INDEX_SIZE_ERR',
50
+ HierarchyRequestError: 'HIERARCHY_REQUEST_ERR',
51
+ WrongDocumentError: 'WRONG_DOCUMENT_ERR',
52
+ InvalidCharacterError: 'INVALID_CHARACTER_ERR',
53
+ NoModificationAllowedError: 'NO_MODIFICATION_ALLOWED_ERR',
54
+ NotFoundError: 'NOT_FOUND_ERR',
55
+ NotSupportedError: 'NOT_SUPPORTED_ERR',
56
+ InUseAttributeError: 'INUSE_ATTRIBUTE_ERR',
57
+ InvalidStateError: 'INVALID_STATE_ERR',
58
+ SyntaxError: 'SYNTAX_ERR',
59
+ InvalidModificationError: 'INVALID_MODIFICATION_ERR',
60
+ NamespaceError: 'NAMESPACE_ERR',
61
+ InvalidAccessError: 'INVALID_ACCESS_ERR',
62
+ TypeMismatchError: 'TYPE_MISMATCH_ERR',
63
+ SecurityError: 'SECURITY_ERR',
64
+ NetworkError: 'NETWORK_ERR',
65
+ InvalidNodeTypeError: 'INVALID_NODE_TYPE_ERR',
66
+ DataCloneError: 'DATA_CLONE_ERR',
67
+ };
68
+ Object.keys(STATIC_CONST_NAMES).forEach(function(name) {
69
+ var constName = STATIC_CONST_NAMES[name];
70
+ var code = LEGACY_CODES[name];
71
+ Object.defineProperty(DOMException, constName, { value: code, enumerable: true });
72
+ Object.defineProperty(DOMException.prototype, constName, { value: code, enumerable: true });
73
+ });
74
+
75
+ globalThis.DOMException = DOMException;
76
+ })();
77
+ )JS";
78
+
79
+ } // namespace
80
+
81
+ void DOMExceptionBindings::install(JSContext* ctx) {
82
+ JSValue result = JS_Eval(ctx, kDOMExceptionBootstrapScript, strlen(kDOMExceptionBootstrapScript),
83
+ "<dom-exception-bootstrap>", JS_EVAL_TYPE_GLOBAL);
84
+ if (JS_IsException(result)) {
85
+ JS_FreeValue(ctx, JS_GetException(ctx));
86
+ }
87
+ JS_FreeValue(ctx, result);
88
+ }
89
+
90
+ JSValue throw_dom_exception(JSContext* ctx, const char* name, const char* message) {
91
+ JSValue global = JS_GetGlobalObject(ctx);
92
+ JSValue ctor = JS_GetPropertyStr(ctx, global, "DOMException");
93
+ JS_FreeValue(ctx, global);
94
+
95
+ if (!JS_IsFunction(ctx, ctor)) {
96
+ JS_FreeValue(ctx, ctor);
97
+ return JS_ThrowTypeError(ctx, "%s: %s", name, message);
98
+ }
99
+
100
+ JSValue args[2] = { JS_NewString(ctx, message), JS_NewString(ctx, name) };
101
+ JSValue exc = JS_CallConstructor(ctx, ctor, 2, args);
102
+ JS_FreeValue(ctx, args[0]);
103
+ JS_FreeValue(ctx, args[1]);
104
+ JS_FreeValue(ctx, ctor);
105
+
106
+ if (JS_IsException(exc)) return JS_EXCEPTION;
107
+ JS_Throw(ctx, exc);
108
+ return JS_EXCEPTION;
109
+ }
110
+
111
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,20 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers globalThis.DOMException (extends Error, with the legacy numeric
8
+ // `.code` field). Installed first since every other binding module that
9
+ // throws a named DOM error (NotFoundError, SyntaxError, InvalidCharacterError,
10
+ // ...) depends on the constructor already existing on globalThis.
11
+ struct DOMExceptionBindings {
12
+ static void install(JSContext* ctx);
13
+ };
14
+
15
+ // Constructs `new DOMException(message, name)` via the globalThis constructor
16
+ // and throws it (mirrors JS_ThrowTypeError's calling convention: always
17
+ // returns JS_EXCEPTION so call sites can `return throw_dom_exception(...)`).
18
+ JSValue throw_dom_exception(JSContext* ctx, const char* name, const char* message);
19
+
20
+ } // namespace margelo::nitro::nitrojsdom