@salve-software/react-native-nitro-jsdom 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/android/CMakeLists.txt +14 -0
  3. package/cpp/HybridHtmlSandbox.cpp +16 -3
  4. package/cpp/HybridHtmlSandbox.hpp +1 -1
  5. package/cpp/lexbor/LexborDocument.cpp +217 -67
  6. package/cpp/lexbor/LexborDocument.hpp +29 -10
  7. package/cpp/quickjs/DOMBindings.cpp +28 -0
  8. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  9. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  10. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  11. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  12. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  13. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  14. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  15. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  17. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
  18. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  19. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  20. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  22. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  24. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  25. package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
  26. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  27. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  28. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  29. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  30. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  32. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  34. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  35. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  36. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  38. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  40. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  41. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  42. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  43. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  44. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  46. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  48. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  50. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  51. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  53. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  54. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  55. package/package.json +2 -2
  56. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  57. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -34,11 +34,16 @@ class LexborDocument;
34
34
  // node class (Node traversal only). This prevents non-element nodes from
35
35
  // reaching lxb_dom_element_* calls.
36
36
  //
37
+ // js_shadow_root_class_id wraps lxb_dom_shadow_root_t* (see ShadowRootBindings).
38
+ //
37
39
  // unwrap_element() returns non-null only for element-class objects.
38
- // unwrap_node() returns non-null for either class (shared Node methods).
40
+ // unwrap_node() returns non-null for any of the three (shared Node methods
41
+ // like appendChild/childNodes work the same way on a
42
+ // ShadowRoot, since it embeds a document fragment's layout).
39
43
 
40
44
  extern JSClassID js_element_class_id;
41
45
  extern JSClassID js_node_class_id;
46
+ extern JSClassID js_shadow_root_class_id;
42
47
 
43
48
  JSValue make_element(JSContext* ctx, void* node_or_el);
44
49
  JSValue make_element_array(JSContext* ctx, const std::vector<void*>& elements);
@@ -57,6 +62,12 @@ void invalidate_node_cache(JSContext* ctx, RuntimeContext* rctx, void* node);
57
62
  void invalidate_node_cache_batch(JSContext* ctx, RuntimeContext* rctx, const std::vector<void*>& nodes);
58
63
  void clear_node_cache(JSContext* ctx, RuntimeContext* rctx);
59
64
 
65
+ // Same as invalidate_node_cache_batch, but also invalidates every descendant
66
+ // of each given node. Use before destroying a subtree with
67
+ // lxb_dom_node_destroy_deep(): that frees descendants too, and a JS wrapper
68
+ // left pointing at a freed descendant is a use-after-free waiting to happen.
69
+ void invalidate_node_cache_deep_batch(JSContext* ctx, RuntimeContext* rctx, const std::vector<void*>& roots);
70
+
60
71
  // ── Accessor property helper ──────────────────────────────────────────────────
61
72
 
62
73
  using GetterFn = JSValue (*)(JSContext*, JSValue);
@@ -64,6 +75,11 @@ using SetterFn = JSValue (*)(JSContext*, JSValue, JSValue);
64
75
 
65
76
  void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter, SetterFn setter = nullptr);
66
77
 
78
+ // ── Global constructor helper (instanceof support) ────────────────────────────
79
+
80
+ JSValue js_illegal_constructor(JSContext* ctx, JSValue this_val, int argc, JSValue* argv);
81
+ JSValue define_global_constructor(JSContext* ctx, const char* name, JSValue proto);
82
+
67
83
  // ── Misc shared helpers ───────────────────────────────────────────────────────
68
84
 
69
85
  // Reads a boolean-ish property off any JS object (used by Event dispatch to read
@@ -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
- // Update the stack reference so QuickJS doesn't false-positive overflow
402
- // when evaluate() runs on a different thread than initialize().
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,12 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ class AbortBindings {
8
+ public:
9
+ static void install(JSContext* ctx);
10
+ };
11
+
12
+ } // 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