@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
|
@@ -90,6 +90,14 @@ QuickJSRuntime::~QuickJSRuntime() {
|
|
|
90
90
|
|
|
91
91
|
// Free node wrapper cache
|
|
92
92
|
clear_node_cache(ctx, _ctxState.get());
|
|
93
|
+
|
|
94
|
+
// Free the cached document.doctype wrapper
|
|
95
|
+
if (_ctxState->doctype_wrapper) {
|
|
96
|
+
JSValue* stored = static_cast<JSValue*>(_ctxState->doctype_wrapper);
|
|
97
|
+
JS_FreeValue(ctx, *stored);
|
|
98
|
+
delete stored;
|
|
99
|
+
_ctxState->doctype_wrapper = nullptr;
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
if (_context) JS_FreeContext(reinterpret_cast<JSContext*>(_context));
|
|
@@ -137,7 +145,7 @@ void QuickJSRuntime::cleanupTimers() {
|
|
|
137
145
|
|
|
138
146
|
// ── initialize ─────────────────────────────────────────────────────────────────
|
|
139
147
|
|
|
140
|
-
void QuickJSRuntime::initialize(const std::string& url) {
|
|
148
|
+
void QuickJSRuntime::initialize(const std::string& url, bool pretendToBeVisual) {
|
|
141
149
|
JSRuntime* rt = JS_NewRuntime();
|
|
142
150
|
if (!rt) throw std::runtime_error("QuickJS: failed to create runtime");
|
|
143
151
|
JSContext* ctx = JS_NewContext(rt);
|
|
@@ -151,6 +159,7 @@ void QuickJSRuntime::initialize(const std::string& url) {
|
|
|
151
159
|
// Create RuntimeContext and attach to QuickJS context opaque
|
|
152
160
|
_ctxState = std::make_unique<RuntimeContext>();
|
|
153
161
|
_ctxState->runtime = this;
|
|
162
|
+
_ctxState->pretend_to_be_visual = pretendToBeVisual;
|
|
154
163
|
_ctxState->mutation_observers = std::make_unique<MutationObservers>();
|
|
155
164
|
JS_SetContextOpaque(ctx, _ctxState.get());
|
|
156
165
|
|
|
@@ -396,10 +405,16 @@ void QuickJSRuntime::fireTimer(Timer* t) {
|
|
|
396
405
|
std::string QuickJSRuntime::evaluate(const std::string& script) {
|
|
397
406
|
JSRuntime* rt = reinterpret_cast<JSRuntime*>(_runtime);
|
|
398
407
|
JSContext* ctx = reinterpret_cast<JSContext*>(_context);
|
|
399
|
-
std::lock_guard<std::mutex> lock(_mutex);
|
|
400
408
|
|
|
401
|
-
|
|
402
|
-
|
|
409
|
+
std::unique_lock<std::mutex> lock(_mutex, std::try_to_lock);
|
|
410
|
+
if (!lock.owns_lock()) {
|
|
411
|
+
throw std::runtime_error(
|
|
412
|
+
"QuickJSRuntime: evaluate() called reentrantly. A callback (onFetch/onAlert/onConfirm/onPrompt) "
|
|
413
|
+
"attempted to call dom.evaluate() again while an evaluate() call was already in progress on this "
|
|
414
|
+
"instance. QuickJS is not re-entrant."
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
403
418
|
JS_UpdateStackTop(rt);
|
|
404
419
|
|
|
405
420
|
JSValue result = JS_Eval(ctx, script.data(), script.size(), "<eval>", JS_EVAL_TYPE_GLOBAL);
|
|
@@ -66,10 +66,31 @@ struct RuntimeContext {
|
|
|
66
66
|
Storage local_storage;
|
|
67
67
|
Storage session_storage;
|
|
68
68
|
|
|
69
|
+
// In-memory document.cookie jar (name -> value). No real navigation/origin
|
|
70
|
+
// model exists in this sandbox, so cookie attributes (expires/path/domain/
|
|
71
|
+
// secure/samesite) are parsed off the setter's input and discarded rather
|
|
72
|
+
// than enforced — see CookieBindings.
|
|
73
|
+
Storage cookie_jar;
|
|
74
|
+
|
|
75
|
+
bool pretend_to_be_visual { false };
|
|
76
|
+
double time_origin_ms { 0 };
|
|
77
|
+
|
|
69
78
|
// node pointer → heap-allocated JSValue* (DupValue'd strong ref)
|
|
70
79
|
// Ensures the same native node always returns the same JS wrapper object.
|
|
71
80
|
std::unordered_map<void*, void*> node_wrapper_cache;
|
|
72
81
|
|
|
82
|
+
// document.doctype's plain-object wrapper (heap-allocated JSValue*,
|
|
83
|
+
// DupValue'd strong ref) — not routed through node_wrapper_cache since
|
|
84
|
+
// it isn't keyed by a native node pointer (see DocumentBindings.cpp).
|
|
85
|
+
// Built lazily on first access and reused after that, so repeated
|
|
86
|
+
// `document.doctype` reads return the same JS object (identity-stable).
|
|
87
|
+
void* doctype_wrapper { nullptr };
|
|
88
|
+
|
|
89
|
+
// host element pointer (lxb_dom_element_t*) → its shadow root
|
|
90
|
+
// (lxb_dom_shadow_root_t*). Lexbor's element struct has no built-in
|
|
91
|
+
// back-pointer to an attached shadow root, so we track it ourselves.
|
|
92
|
+
std::unordered_map<void*, void*> element_shadow_roots;
|
|
93
|
+
|
|
73
94
|
~RuntimeContext() = default;
|
|
74
95
|
};
|
|
75
96
|
|
|
@@ -80,7 +101,7 @@ public:
|
|
|
80
101
|
QuickJSRuntime();
|
|
81
102
|
~QuickJSRuntime();
|
|
82
103
|
|
|
83
|
-
void initialize(const std::string& url);
|
|
104
|
+
void initialize(const std::string& url, bool pretendToBeVisual);
|
|
84
105
|
void bindDocument(LexborDocument* document);
|
|
85
106
|
std::string evaluate(const std::string& script);
|
|
86
107
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#include "AbortBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kAbortBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function makeAbortError() {
|
|
11
|
+
var e = new Error('signal is aborted without reason');
|
|
12
|
+
e.name = 'AbortError';
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
function makeTimeoutError() {
|
|
16
|
+
var e = new Error('signal timed out');
|
|
17
|
+
e.name = 'TimeoutError';
|
|
18
|
+
return e;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function AbortSignal() {
|
|
22
|
+
this.aborted = false;
|
|
23
|
+
this.reason = undefined;
|
|
24
|
+
this._listeners = [];
|
|
25
|
+
this._onabort = null;
|
|
26
|
+
this._onabortListener = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Object.defineProperty(AbortSignal.prototype, 'onabort', {
|
|
30
|
+
get: function() { return this._onabort; },
|
|
31
|
+
set: function(fn) {
|
|
32
|
+
if (this._onabortListener) {
|
|
33
|
+
var idx = this._listeners.indexOf(this._onabortListener);
|
|
34
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
35
|
+
}
|
|
36
|
+
this._onabort = typeof fn === 'function' ? fn : null;
|
|
37
|
+
if (this._onabort) {
|
|
38
|
+
var self = this;
|
|
39
|
+
this._onabortListener = function(evt) { self._onabort(evt); };
|
|
40
|
+
this._listeners.push(this._onabortListener);
|
|
41
|
+
} else {
|
|
42
|
+
this._onabortListener = null;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
AbortSignal.prototype.addEventListener = function(type, cb) {
|
|
50
|
+
if (type !== 'abort' || typeof cb !== 'function') return;
|
|
51
|
+
if (this._listeners.indexOf(cb) === -1) this._listeners.push(cb);
|
|
52
|
+
};
|
|
53
|
+
AbortSignal.prototype.removeEventListener = function(type, cb) {
|
|
54
|
+
if (type !== 'abort') return;
|
|
55
|
+
var idx = this._listeners.indexOf(cb);
|
|
56
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
57
|
+
};
|
|
58
|
+
AbortSignal.prototype.dispatchEvent = function(evt) {
|
|
59
|
+
if (evt && evt.type === 'abort') this._fire();
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
AbortSignal.prototype.throwIfAborted = function() {
|
|
63
|
+
if (this.aborted) throw this.reason;
|
|
64
|
+
};
|
|
65
|
+
AbortSignal.prototype._fire = function() {
|
|
66
|
+
var evt = new Event('abort');
|
|
67
|
+
evt.target = this;
|
|
68
|
+
this._listeners.slice().forEach(function(cb) {
|
|
69
|
+
try { cb(evt); } catch (e) {}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
AbortSignal.prototype._abort = function(reason) {
|
|
73
|
+
if (this.aborted) return;
|
|
74
|
+
this.aborted = true;
|
|
75
|
+
this.reason = reason !== undefined ? reason : makeAbortError();
|
|
76
|
+
this._fire();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
AbortSignal.abort = function(reason) {
|
|
80
|
+
var s = new AbortSignal();
|
|
81
|
+
s._abort(reason);
|
|
82
|
+
return s;
|
|
83
|
+
};
|
|
84
|
+
AbortSignal.timeout = function(ms) {
|
|
85
|
+
var s = new AbortSignal();
|
|
86
|
+
setTimeout(function() { s._abort(makeTimeoutError()); }, ms);
|
|
87
|
+
return s;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
globalThis.AbortSignal = AbortSignal;
|
|
91
|
+
|
|
92
|
+
function AbortController() {
|
|
93
|
+
this.signal = new AbortSignal();
|
|
94
|
+
}
|
|
95
|
+
AbortController.prototype.abort = function(reason) {
|
|
96
|
+
this.signal._abort(reason);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
globalThis.AbortController = AbortController;
|
|
100
|
+
})();
|
|
101
|
+
)JS";
|
|
102
|
+
|
|
103
|
+
} // namespace
|
|
104
|
+
|
|
105
|
+
void AbortBindings::install(JSContext* ctx) {
|
|
106
|
+
JSValue result = JS_Eval(ctx, kAbortBootstrapScript, strlen(kAbortBootstrapScript),
|
|
107
|
+
"<abort-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
108
|
+
if (JS_IsException(result)) {
|
|
109
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
110
|
+
}
|
|
111
|
+
JS_FreeValue(ctx, result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#include "BlobBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kBlobBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function partToBytes(part) {
|
|
11
|
+
if (part instanceof ArrayBuffer) {
|
|
12
|
+
return Array.prototype.slice.call(new Uint8Array(part));
|
|
13
|
+
}
|
|
14
|
+
if (part && typeof part.byteLength === 'number' && part.buffer !== undefined) {
|
|
15
|
+
return Array.prototype.slice.call(new Uint8Array(part.buffer, part.byteOffset, part.byteLength));
|
|
16
|
+
}
|
|
17
|
+
if (part instanceof Blob) {
|
|
18
|
+
return part._bytes.slice();
|
|
19
|
+
}
|
|
20
|
+
return Array.prototype.slice.call(new TextEncoder().encode(String(part)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Blob(parts, options) {
|
|
24
|
+
var bytes = [];
|
|
25
|
+
if (parts) {
|
|
26
|
+
for (var i = 0; i < parts.length; i++) {
|
|
27
|
+
bytes = bytes.concat(partToBytes(parts[i]));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this._bytes = bytes;
|
|
31
|
+
this.size = bytes.length;
|
|
32
|
+
this.type = (options && options.type) ? String(options.type) : '';
|
|
33
|
+
}
|
|
34
|
+
Blob.prototype.slice = function(start, end, contentType) {
|
|
35
|
+
var sliced = this._bytes.slice(start, end);
|
|
36
|
+
var result = new Blob([]);
|
|
37
|
+
result._bytes = sliced;
|
|
38
|
+
result.size = sliced.length;
|
|
39
|
+
result.type = contentType ? String(contentType) : '';
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
Blob.prototype.text = function() {
|
|
43
|
+
var bytes = this._bytes;
|
|
44
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
45
|
+
var view = new Uint8Array(buf);
|
|
46
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
47
|
+
return Promise.resolve(new TextDecoder().decode(buf));
|
|
48
|
+
};
|
|
49
|
+
Blob.prototype.arrayBuffer = function() {
|
|
50
|
+
var bytes = this._bytes;
|
|
51
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
52
|
+
var view = new Uint8Array(buf);
|
|
53
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
54
|
+
return Promise.resolve(buf);
|
|
55
|
+
};
|
|
56
|
+
globalThis.Blob = Blob;
|
|
57
|
+
|
|
58
|
+
function FileReader() {
|
|
59
|
+
this.readyState = 0;
|
|
60
|
+
this.result = null;
|
|
61
|
+
this.error = null;
|
|
62
|
+
this.onload = null;
|
|
63
|
+
this.onloadend = null;
|
|
64
|
+
this.onerror = null;
|
|
65
|
+
this._listeners = {};
|
|
66
|
+
}
|
|
67
|
+
FileReader.EMPTY = 0;
|
|
68
|
+
FileReader.LOADING = 1;
|
|
69
|
+
FileReader.DONE = 2;
|
|
70
|
+
FileReader.prototype.addEventListener = function(type, cb) {
|
|
71
|
+
if (!this._listeners[type]) this._listeners[type] = [];
|
|
72
|
+
if (this._listeners[type].indexOf(cb) === -1) this._listeners[type].push(cb);
|
|
73
|
+
};
|
|
74
|
+
FileReader.prototype.removeEventListener = function(type, cb) {
|
|
75
|
+
var list = this._listeners[type];
|
|
76
|
+
if (!list) return;
|
|
77
|
+
var idx = list.indexOf(cb);
|
|
78
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
79
|
+
};
|
|
80
|
+
FileReader.prototype._fire = function(type) {
|
|
81
|
+
var evt = { type: type, target: this };
|
|
82
|
+
var handlerProp = 'on' + type;
|
|
83
|
+
if (typeof this[handlerProp] === 'function') this[handlerProp](evt);
|
|
84
|
+
var list = this._listeners[type];
|
|
85
|
+
if (list) list.slice().forEach(function(cb) { cb(evt); });
|
|
86
|
+
};
|
|
87
|
+
FileReader.prototype._read = function(blob, mode) {
|
|
88
|
+
var self = this;
|
|
89
|
+
self.readyState = 1;
|
|
90
|
+
setTimeout(function() {
|
|
91
|
+
if (!blob) {
|
|
92
|
+
self.error = new Error('FileReader was given no blob to read');
|
|
93
|
+
self.readyState = 2;
|
|
94
|
+
self._fire('error');
|
|
95
|
+
self._fire('loadend');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
var bytes = blob._bytes || [];
|
|
99
|
+
if (mode === 'text') {
|
|
100
|
+
var buf = new ArrayBuffer(bytes.length);
|
|
101
|
+
var view = new Uint8Array(buf);
|
|
102
|
+
for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
|
|
103
|
+
self.result = new TextDecoder().decode(buf);
|
|
104
|
+
} else if (mode === 'dataurl') {
|
|
105
|
+
var binary = '';
|
|
106
|
+
for (var j = 0; j < bytes.length; j++) binary += String.fromCharCode(bytes[j]);
|
|
107
|
+
self.result = 'data:' + (blob.type || '') + ';base64,' + btoa(binary);
|
|
108
|
+
} else if (mode === 'arraybuffer') {
|
|
109
|
+
var arrBuf = new ArrayBuffer(bytes.length);
|
|
110
|
+
var arrView = new Uint8Array(arrBuf);
|
|
111
|
+
for (var k = 0; k < bytes.length; k++) arrView[k] = bytes[k];
|
|
112
|
+
self.result = arrBuf;
|
|
113
|
+
}
|
|
114
|
+
self.readyState = 2;
|
|
115
|
+
self._fire('load');
|
|
116
|
+
self._fire('loadend');
|
|
117
|
+
}, 0);
|
|
118
|
+
};
|
|
119
|
+
FileReader.prototype.readAsText = function(blob) { this._read(blob, 'text'); };
|
|
120
|
+
FileReader.prototype.readAsDataURL = function(blob) { this._read(blob, 'dataurl'); };
|
|
121
|
+
FileReader.prototype.readAsArrayBuffer = function(blob) { this._read(blob, 'arraybuffer'); };
|
|
122
|
+
globalThis.FileReader = FileReader;
|
|
123
|
+
})();
|
|
124
|
+
)JS";
|
|
125
|
+
|
|
126
|
+
} // namespace
|
|
127
|
+
|
|
128
|
+
void BlobBindings::install(JSContext* ctx) {
|
|
129
|
+
JSValue result = JS_Eval(ctx, kBlobBootstrapScript, strlen(kBlobBootstrapScript),
|
|
130
|
+
"<blob-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
131
|
+
if (JS_IsException(result)) {
|
|
132
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
133
|
+
}
|
|
134
|
+
JS_FreeValue(ctx, result);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "quickjs.h"
|
|
4
|
+
|
|
5
|
+
namespace margelo::nitro::nitrojsdom {
|
|
6
|
+
|
|
7
|
+
// Registers globalThis.Blob and globalThis.FileReader. Pure JS on top of
|
|
8
|
+
// TextEncoder/TextDecoder and btoa, so must run after TextEncodingBindings
|
|
9
|
+
// and WindowBindings.
|
|
10
|
+
struct BlobBindings {
|
|
11
|
+
static void install(JSContext* ctx);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#include "CSSOMBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kCSSOMBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function stripComments(text) {
|
|
11
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function camelToKebab(name) {
|
|
15
|
+
return name.replace(/[A-Z]/g, function(c) { return '-' + c.toLowerCase(); });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Splits a stylesheet's raw text into top-level rule descriptors, tracking
|
|
19
|
+
// brace depth so a nested block (e.g. the rules inside @media) is captured
|
|
20
|
+
// as one opaque chunk rather than mis-parsed as a sibling rule.
|
|
21
|
+
function splitTopLevelRules(cssText) {
|
|
22
|
+
var text = stripComments(String(cssText === undefined ? '' : cssText));
|
|
23
|
+
var descriptors = [];
|
|
24
|
+
var i = 0;
|
|
25
|
+
var len = text.length;
|
|
26
|
+
while (i < len) {
|
|
27
|
+
while (i < len && /\s/.test(text.charAt(i))) i++;
|
|
28
|
+
if (i >= len) break;
|
|
29
|
+
var start = i;
|
|
30
|
+
var depth = 0;
|
|
31
|
+
var j = i;
|
|
32
|
+
while (j < len) {
|
|
33
|
+
var c = text.charAt(j);
|
|
34
|
+
if (c === '{') {
|
|
35
|
+
depth++;
|
|
36
|
+
} else if (c === '}') {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth <= 0) { j++; break; }
|
|
39
|
+
} else if (c === ';' && depth === 0) {
|
|
40
|
+
j++;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
var raw = text.slice(start, j);
|
|
46
|
+
if (raw.trim()) descriptors.push(parseOneRule(raw));
|
|
47
|
+
i = j;
|
|
48
|
+
}
|
|
49
|
+
return descriptors;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseOneRule(raw) {
|
|
53
|
+
var trimmed = raw.trim();
|
|
54
|
+
var braceIdx = trimmed.indexOf('{');
|
|
55
|
+
if (trimmed.charAt(0) === '@' || braceIdx === -1) {
|
|
56
|
+
return { kind: 'unknown', cssText: trimmed };
|
|
57
|
+
}
|
|
58
|
+
var selectorText = trimmed.slice(0, braceIdx).trim();
|
|
59
|
+
var body = trimmed.slice(braceIdx + 1, trimmed.length - 1);
|
|
60
|
+
return { kind: 'style', selectorText: selectorText, declarationText: body.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parses "color: red; font-size: 12px" into ordered [name, value] pairs —
|
|
64
|
+
// same informal model StyleBindings.cpp uses for inline element.style.
|
|
65
|
+
function parseDeclarations(text) {
|
|
66
|
+
var pairs = [];
|
|
67
|
+
var parts = String(text === undefined ? '' : text).split(';');
|
|
68
|
+
for (var i = 0; i < parts.length; i++) {
|
|
69
|
+
var part = parts[i].trim();
|
|
70
|
+
if (!part) continue;
|
|
71
|
+
var colonIdx = part.indexOf(':');
|
|
72
|
+
if (colonIdx === -1) continue;
|
|
73
|
+
var prop = part.slice(0, colonIdx).trim().toLowerCase();
|
|
74
|
+
var value = part.slice(colonIdx + 1).trim();
|
|
75
|
+
if (prop) pairs.push([prop, value]);
|
|
76
|
+
}
|
|
77
|
+
return pairs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeRuleStyle(initialText) {
|
|
81
|
+
var pairs = parseDeclarations(initialText);
|
|
82
|
+
|
|
83
|
+
var target = {
|
|
84
|
+
getPropertyValue: function(name) {
|
|
85
|
+
var kebab = camelToKebab(String(name));
|
|
86
|
+
for (var i = 0; i < pairs.length; i++) if (pairs[i][0] === kebab) return pairs[i][1];
|
|
87
|
+
return '';
|
|
88
|
+
},
|
|
89
|
+
setProperty: function(name, value) {
|
|
90
|
+
var kebab = camelToKebab(String(name));
|
|
91
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
92
|
+
if (pairs[i][0] === kebab) { pairs[i][1] = String(value); return; }
|
|
93
|
+
}
|
|
94
|
+
pairs.push([kebab, String(value)]);
|
|
95
|
+
},
|
|
96
|
+
removeProperty: function(name) {
|
|
97
|
+
var kebab = camelToKebab(String(name));
|
|
98
|
+
var old = '';
|
|
99
|
+
var next = [];
|
|
100
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
101
|
+
if (pairs[i][0] === kebab && !old) { old = pairs[i][1]; continue; }
|
|
102
|
+
next.push(pairs[i]);
|
|
103
|
+
}
|
|
104
|
+
pairs = next;
|
|
105
|
+
return old;
|
|
106
|
+
},
|
|
107
|
+
get cssText() {
|
|
108
|
+
var out = '';
|
|
109
|
+
for (var i = 0; i < pairs.length; i++) out += pairs[i][0] + ': ' + pairs[i][1] + '; ';
|
|
110
|
+
return out.trim();
|
|
111
|
+
},
|
|
112
|
+
set cssText(text) {
|
|
113
|
+
pairs = parseDeclarations(text);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return new Proxy(target, {
|
|
118
|
+
get: function(t, prop, receiver) {
|
|
119
|
+
if (typeof prop === 'string' && !(prop in t)) {
|
|
120
|
+
var kebab = camelToKebab(prop);
|
|
121
|
+
for (var i = 0; i < pairs.length; i++) if (pairs[i][0] === kebab) return pairs[i][1];
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return Reflect.get(t, prop, receiver);
|
|
125
|
+
},
|
|
126
|
+
set: function(t, prop, value, receiver) {
|
|
127
|
+
if (typeof prop === 'string' && !(prop in t)) {
|
|
128
|
+
t.setProperty(prop, value);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return Reflect.set(t, prop, value, receiver);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function CSSRule() {}
|
|
137
|
+
CSSRule.STYLE_RULE = 1;
|
|
138
|
+
CSSRule.MEDIA_RULE = 4;
|
|
139
|
+
CSSRule.prototype.type = 0;
|
|
140
|
+
|
|
141
|
+
function CSSStyleRule(selectorText, declarationText, parentStyleSheet) {
|
|
142
|
+
this.selectorText = selectorText;
|
|
143
|
+
this.style = makeRuleStyle(declarationText);
|
|
144
|
+
this.parentStyleSheet = parentStyleSheet || null;
|
|
145
|
+
this.type = CSSRule.STYLE_RULE;
|
|
146
|
+
}
|
|
147
|
+
CSSStyleRule.prototype = Object.create(CSSRule.prototype);
|
|
148
|
+
CSSStyleRule.prototype.constructor = CSSStyleRule;
|
|
149
|
+
Object.defineProperty(CSSStyleRule.prototype, 'cssText', {
|
|
150
|
+
get: function() { return this.selectorText + ' { ' + this.style.cssText + ' }'; },
|
|
151
|
+
configurable: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
function makeUnknownRule(cssText, parentStyleSheet) {
|
|
155
|
+
return { type: 0, cssText: cssText, parentStyleSheet: parentStyleSheet || null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ruleFromDescriptor(d, sheet) {
|
|
159
|
+
return d.kind === 'style'
|
|
160
|
+
? new CSSStyleRule(d.selectorText, d.declarationText, sheet)
|
|
161
|
+
: makeUnknownRule(d.cssText, sheet);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeRuleList(rules) {
|
|
165
|
+
var arr = rules.slice();
|
|
166
|
+
arr.item = function(i) { return this[i] !== undefined ? this[i] : null; };
|
|
167
|
+
return arr;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CSSStyleSheet(ownerNode) {
|
|
171
|
+
this.ownerNode = ownerNode || null;
|
|
172
|
+
this.cssRules = makeRuleList([]);
|
|
173
|
+
this.rules = this.cssRules;
|
|
174
|
+
}
|
|
175
|
+
CSSStyleSheet.prototype.insertRule = function(ruleText, index) {
|
|
176
|
+
var idx = (index === undefined) ? this.cssRules.length : index;
|
|
177
|
+
if (idx < 0 || idx > this.cssRules.length) {
|
|
178
|
+
throw new DOMException(
|
|
179
|
+
'The index provided (' + idx + ') is not in the allowed range.', 'IndexSizeError');
|
|
180
|
+
}
|
|
181
|
+
var rule = ruleFromDescriptor(parseOneRule(String(ruleText)), this);
|
|
182
|
+
Array.prototype.splice.call(this.cssRules, idx, 0, rule);
|
|
183
|
+
return idx;
|
|
184
|
+
};
|
|
185
|
+
CSSStyleSheet.prototype.deleteRule = function(index) {
|
|
186
|
+
if (index < 0 || index >= this.cssRules.length) {
|
|
187
|
+
throw new DOMException(
|
|
188
|
+
'The index provided (' + index + ') is not in the allowed range.', 'IndexSizeError');
|
|
189
|
+
}
|
|
190
|
+
Array.prototype.splice.call(this.cssRules, index, 1);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function parseStylesheetInto(sheet, cssText) {
|
|
194
|
+
var descriptors = splitTopLevelRules(cssText);
|
|
195
|
+
for (var i = 0; i < descriptors.length; i++) {
|
|
196
|
+
sheet.cssRules.push(ruleFromDescriptor(descriptors[i], sheet));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
globalThis.CSSRule = CSSRule;
|
|
201
|
+
globalThis.CSSStyleRule = CSSStyleRule;
|
|
202
|
+
globalThis.CSSStyleSheet = CSSStyleSheet;
|
|
203
|
+
|
|
204
|
+
Object.defineProperty(Element.prototype, 'sheet', {
|
|
205
|
+
get: function() {
|
|
206
|
+
if (this.tagName !== 'STYLE') return null;
|
|
207
|
+
if (!this._sheet) {
|
|
208
|
+
var sheet = new CSSStyleSheet(this);
|
|
209
|
+
parseStylesheetInto(sheet, this.textContent);
|
|
210
|
+
this._sheet = sheet;
|
|
211
|
+
}
|
|
212
|
+
return this._sheet;
|
|
213
|
+
},
|
|
214
|
+
configurable: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
Object.defineProperty(document, 'styleSheets', {
|
|
218
|
+
get: function() {
|
|
219
|
+
var styleEls = document.querySelectorAll('style');
|
|
220
|
+
var sheets = [];
|
|
221
|
+
for (var i = 0; i < styleEls.length; i++) {
|
|
222
|
+
var sheet = styleEls[i].sheet;
|
|
223
|
+
if (sheet) sheets.push(sheet);
|
|
224
|
+
}
|
|
225
|
+
sheets.item = function(i) { return this[i] !== undefined ? this[i] : null; };
|
|
226
|
+
return sheets;
|
|
227
|
+
},
|
|
228
|
+
configurable: true,
|
|
229
|
+
});
|
|
230
|
+
})();
|
|
231
|
+
)JS";
|
|
232
|
+
|
|
233
|
+
} // namespace
|
|
234
|
+
|
|
235
|
+
void CSSOMBindings::install(JSContext* ctx) {
|
|
236
|
+
JSValue result = JS_Eval(ctx, kCSSOMBootstrapScript, strlen(kCSSOMBootstrapScript),
|
|
237
|
+
"<cssom-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
238
|
+
if (JS_IsException(result)) {
|
|
239
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
240
|
+
}
|
|
241
|
+
JS_FreeValue(ctx, result);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
} // 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 a minimal CSSOM: globalThis.CSSRule/CSSStyleRule/CSSStyleSheet,
|
|
8
|
+
// Element.prototype.sheet (tag-checked for "style"), and document.styleSheets.
|
|
9
|
+
//
|
|
10
|
+
// Rules are split by a hand-rolled top-level brace scanner rather than
|
|
11
|
+
// Lexbor's CSS rule/declaration AST (lxb_css_rule_t) — mirrors how
|
|
12
|
+
// StyleBindings.cpp already treats inline "style" text as informally-parsed
|
|
13
|
+
// property:value pairs rather than integrating Lexbor's typed CSS value
|
|
14
|
+
// engine. At-rules (@media, @keyframes, ...) are captured as opaque stub
|
|
15
|
+
// rules (cssText only, no nested-rule access) rather than fully parsed.
|
|
16
|
+
//
|
|
17
|
+
// Must run after ElementBindings (globalThis.Element) and DocumentBindings
|
|
18
|
+
// (globalThis.document).
|
|
19
|
+
struct CSSOMBindings {
|
|
20
|
+
static void install(JSContext* ctx);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "ClassListBindings.hpp"
|
|
2
|
+
#include "DOMExceptionBindings.hpp"
|
|
2
3
|
#include "../DOMBindingsInternal.hpp"
|
|
3
4
|
#include "../QuickJSRuntime.hpp"
|
|
4
5
|
#include "../MutationObservers.hpp"
|
|
@@ -36,13 +37,13 @@ std::string join_classes(const std::vector<std::string>& v) {
|
|
|
36
37
|
// Validates a DOMTokenList token: throws on empty or whitespace-containing token.
|
|
37
38
|
bool validate_token(JSContext* ctx, const char* token) {
|
|
38
39
|
if (!token || token[0] == '\0') {
|
|
39
|
-
|
|
40
|
+
throw_dom_exception(ctx, "SyntaxError", "The token provided must not be empty.");
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
42
43
|
for (const char* p = token; *p; p++) {
|
|
43
44
|
unsigned char c = (unsigned char)*p;
|
|
44
45
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f') {
|
|
45
|
-
|
|
46
|
+
throw_dom_exception(ctx, "InvalidCharacterError", "The token provided contains HTML space characters, which are not valid in tokens.");
|
|
46
47
|
return false;
|
|
47
48
|
}
|
|
48
49
|
}
|