@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,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
|