@salve-software/react-native-nitro-jsdom 1.0.1 → 2.0.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 (66) hide show
  1. package/android/CMakeLists.txt +18 -0
  2. package/cpp/HybridHtmlSandbox.cpp +20 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +279 -67
  5. package/cpp/lexbor/LexborDocument.hpp +43 -10
  6. package/cpp/quickjs/DOMBindings.cpp +37 -1
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +101 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +56 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +32 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +50 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +128 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +164 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +290 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +25 -0
  17. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  18. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  19. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  20. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  22. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  24. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  25. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  26. package/cpp/quickjs/bindings/DocumentBindings.cpp +239 -4
  27. package/cpp/quickjs/bindings/ElementBindings.cpp +556 -44
  28. package/cpp/quickjs/bindings/EventBindings.cpp +414 -8
  29. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  30. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  31. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  32. package/cpp/quickjs/bindings/FetchBindings.cpp +45 -6
  33. package/cpp/quickjs/bindings/FormBindings.cpp +365 -0
  34. package/cpp/quickjs/bindings/FormBindings.hpp +20 -0
  35. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  36. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  37. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +228 -0
  38. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  39. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  40. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  41. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  42. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  43. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  44. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  45. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  46. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  47. package/cpp/quickjs/bindings/TimerBindings.cpp +120 -0
  48. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  49. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  50. package/cpp/quickjs/bindings/UrlBindings.cpp +295 -0
  51. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  52. package/cpp/quickjs/bindings/WindowBindings.cpp +552 -0
  53. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  54. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  55. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  56. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  57. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  58. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  59. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  60. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  61. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  62. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  63. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  64. package/package.json +17 -2
  65. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  66. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -118,7 +118,7 @@ const char* kFetchBootstrapScript = R"JS(
118
118
  this.status = init.status !== undefined ? init.status : 200;
119
119
  this.statusText = init.statusText !== undefined ? init.statusText : '';
120
120
  this.ok = this.status >= 200 && this.status < 300;
121
- this.headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
121
+ this.headers = new Headers(init.headers);
122
122
  this.bodyUsed = false;
123
123
  }
124
124
  Response.prototype.text = function() {
@@ -138,15 +138,54 @@ const char* kFetchBootstrapScript = R"JS(
138
138
  };
139
139
  globalThis.Response = Response;
140
140
 
141
+ function Request(input, init) {
142
+ init = init || {};
143
+ var fromRequest = input instanceof Request;
144
+ if (fromRequest && input.bodyUsed) {
145
+ throw new TypeError('Cannot construct a Request with a Request object that has already been used.');
146
+ }
147
+ this.url = fromRequest ? input.url : String(input);
148
+ this.method = (init.method || (fromRequest ? input.method : 'GET')).toUpperCase();
149
+ this.headers = new Headers(init.headers || (fromRequest ? input.headers : undefined));
150
+ var initBody = (init.body === undefined) ? (fromRequest ? input._body : undefined) : init.body;
151
+ this._body = (initBody === undefined || initBody === null) ? undefined : initBody;
152
+ this.signal = init.signal || (fromRequest ? input.signal : undefined);
153
+ this.bodyUsed = false;
154
+ if (fromRequest) input.bodyUsed = true;
155
+ }
156
+ Request.prototype.text = function() {
157
+ if (this.bodyUsed) return Promise.reject(new TypeError('Body has already been consumed.'));
158
+ this.bodyUsed = true;
159
+ return Promise.resolve(this._body === undefined ? '' : String(this._body));
160
+ };
161
+ Request.prototype.json = function() {
162
+ if (this.bodyUsed) return Promise.reject(new TypeError('Body has already been consumed.'));
163
+ this.bodyUsed = true;
164
+ try {
165
+ return Promise.resolve(JSON.parse(this._body));
166
+ } catch (e) {
167
+ return Promise.reject(e);
168
+ }
169
+ };
170
+ Request.prototype.clone = function() {
171
+ if (this.bodyUsed) throw new TypeError('Failed to execute clone: body is already used.');
172
+ return new Request(this.url, { method: this.method, headers: this.headers, body: this._body, signal: this.signal });
173
+ };
174
+ globalThis.Request = Request;
175
+
141
176
  globalThis.fetch = function(input, init) {
142
177
  return new Promise(function(resolve, reject) {
143
178
  try {
144
- var url = typeof input === 'string' ? input : (input && input.url);
145
- init = init || {};
146
- var method = (init.method || 'GET').toUpperCase();
147
- var headers = normalizeHeaders(init.headers);
179
+ var req = new Request(input, init);
180
+ if (req.signal && req.signal.aborted) {
181
+ reject(req.signal.reason !== undefined ? req.signal.reason : new Error('The operation was aborted'));
182
+ return;
183
+ }
184
+ var url = req.url;
185
+ var method = req.method;
186
+ var headers = normalizeHeaders(req.headers);
148
187
  var headersJson = JSON.stringify(headers);
149
- var body = (init.body === undefined || init.body === null) ? undefined : String(init.body);
188
+ var body = req._body === undefined ? undefined : String(req._body);
150
189
 
151
190
  var raw = __nativeFetchSync(url, method, headersJson, body);
152
191
  if (raw.error) {
@@ -0,0 +1,365 @@
1
+ #include "FormBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kFormBootstrapScript = R"JS(
9
+ (function() {
10
+ function FormData(form) {
11
+ this._entries = [];
12
+ if (form) {
13
+ var els = form.querySelectorAll('input, textarea, select');
14
+ for (var i = 0; i < els.length; i++) {
15
+ var el = els[i];
16
+ var name = el.getAttribute('name');
17
+ if (!name) continue;
18
+ var type = (el.getAttribute('type') || '').toLowerCase();
19
+ if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
20
+ this._entries.push([name, el.value]);
21
+ }
22
+ }
23
+ }
24
+
25
+ FormData.prototype.append = function(name, value) {
26
+ this._entries.push([String(name), String(value)]);
27
+ };
28
+ FormData.prototype.set = function(name, value) {
29
+ name = String(name);
30
+ value = String(value);
31
+ var replaced = false;
32
+ var next = [];
33
+ for (var i = 0; i < this._entries.length; i++) {
34
+ var entry = this._entries[i];
35
+ if (entry[0] !== name) { next.push(entry); continue; }
36
+ if (!replaced) { next.push([name, value]); replaced = true; }
37
+ }
38
+ if (!replaced) next.push([name, value]);
39
+ this._entries = next;
40
+ };
41
+ FormData.prototype.get = function(name) {
42
+ name = String(name);
43
+ for (var i = 0; i < this._entries.length; i++) {
44
+ if (this._entries[i][0] === name) return this._entries[i][1];
45
+ }
46
+ return null;
47
+ };
48
+ FormData.prototype.getAll = function(name) {
49
+ name = String(name);
50
+ var result = [];
51
+ for (var i = 0; i < this._entries.length; i++) {
52
+ if (this._entries[i][0] === name) result.push(this._entries[i][1]);
53
+ }
54
+ return result;
55
+ };
56
+ FormData.prototype.has = function(name) {
57
+ name = String(name);
58
+ for (var i = 0; i < this._entries.length; i++) {
59
+ if (this._entries[i][0] === name) return true;
60
+ }
61
+ return false;
62
+ };
63
+ FormData.prototype.delete = function(name) {
64
+ name = String(name);
65
+ var next = [];
66
+ for (var i = 0; i < this._entries.length; i++) {
67
+ if (this._entries[i][0] !== name) next.push(this._entries[i]);
68
+ }
69
+ this._entries = next;
70
+ };
71
+ FormData.prototype.forEach = function(callback, thisArg) {
72
+ for (var i = 0; i < this._entries.length; i++) {
73
+ callback.call(thisArg, this._entries[i][1], this._entries[i][0], this);
74
+ }
75
+ };
76
+ FormData.prototype.entries = function() {
77
+ var entries = this._entries;
78
+ var i = 0;
79
+ var iter = {
80
+ next: function() {
81
+ if (i >= entries.length) return { done: true, value: undefined };
82
+ return { done: false, value: entries[i++].slice() };
83
+ },
84
+ };
85
+ iter[Symbol.iterator] = function() { return iter; };
86
+ return iter;
87
+ };
88
+ FormData.prototype.keys = function() {
89
+ var it = this.entries();
90
+ var keysIter = {
91
+ next: function() {
92
+ var r = it.next();
93
+ return r.done ? r : { done: false, value: r.value[0] };
94
+ },
95
+ };
96
+ keysIter[Symbol.iterator] = function() { return keysIter; };
97
+ return keysIter;
98
+ };
99
+ FormData.prototype.values = function() {
100
+ var it = this.entries();
101
+ var valuesIter = {
102
+ next: function() {
103
+ var r = it.next();
104
+ return r.done ? r : { done: false, value: r.value[1] };
105
+ },
106
+ };
107
+ valuesIter[Symbol.iterator] = function() { return valuesIter; };
108
+ return valuesIter;
109
+ };
110
+ FormData.prototype[Symbol.iterator] = function() { return this.entries(); };
111
+
112
+ globalThis.FormData = FormData;
113
+
114
+ // ── form.requestSubmit() / form.submit() ──────────────────────────────────
115
+ function isFormElement(el) {
116
+ return !!el && el.tagName === 'FORM';
117
+ }
118
+ Element.prototype.requestSubmit = function(submitter) {
119
+ if (!isFormElement(this)) return;
120
+ var evt = new Event('submit', { bubbles: true, cancelable: true });
121
+ evt.submitter = submitter || null;
122
+ this.dispatchEvent(evt);
123
+ };
124
+ Element.prototype.submit = function() {
125
+ // Per spec, submit() bypasses the "submit" event and constraint validation.
126
+ // There is no real navigation in this sandbox, so this is intentionally inert.
127
+ };
128
+
129
+ // ── Constraint Validation API (ValidityState, checkValidity, ...) ─────────
130
+ // Covers the subset real-world CMS forms actually hit: required, pattern,
131
+ // min/max/minlength/maxlength, type="email"/"url"/"number" and custom
132
+ // validity. `step` is intentionally not modeled (stepMismatch is always
133
+ // false) — it needs the same "default step base" arithmetic per input type
134
+ // that browsers hardcode, which isn't worth it for this sandbox's scope.
135
+ // reportValidity() has no UI to report against here, so it's just an alias
136
+ // for checkValidity() (same outcome jsdom has, for the same reason).
137
+
138
+ var VALIDITY_KEYS = [
139
+ 'valueMissing', 'typeMismatch', 'patternMismatch', 'tooLong', 'tooShort',
140
+ 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput', 'customError',
141
+ ];
142
+
143
+ function ValidityState(flags) {
144
+ this._flags = flags;
145
+ }
146
+ VALIDITY_KEYS.forEach(function(key) {
147
+ Object.defineProperty(ValidityState.prototype, key, {
148
+ enumerable: true,
149
+ get: function() { return !!this._flags[key]; },
150
+ });
151
+ });
152
+ Object.defineProperty(ValidityState.prototype, 'valid', {
153
+ enumerable: true,
154
+ get: function() {
155
+ var flags = this._flags;
156
+ return !VALIDITY_KEYS.some(function(key) { return flags[key]; });
157
+ },
158
+ });
159
+ globalThis.ValidityState = ValidityState;
160
+
161
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
162
+ var URL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/\S+$/;
163
+
164
+ function isFieldElement(el) {
165
+ return el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA';
166
+ }
167
+
168
+ function fieldType(el) {
169
+ if (el.tagName === 'TEXTAREA') return 'textarea';
170
+ if (el.tagName === 'SELECT') return 'select';
171
+ return (el.getAttribute('type') || 'text').toLowerCase();
172
+ }
173
+
174
+ function computeValidityFlags(el) {
175
+ var flags = {};
176
+ if (el._customValidity) flags.customError = true;
177
+
178
+ if (!isFieldElement(el) || el.hasAttribute('disabled')) return flags;
179
+
180
+ var type = fieldType(el);
181
+ if (type === 'hidden') return flags;
182
+
183
+ var required = el.hasAttribute('required');
184
+
185
+ if (type === 'checkbox' || type === 'radio') {
186
+ if (required && !el.checked) flags.valueMissing = true;
187
+ return flags;
188
+ }
189
+
190
+ var value = el.value;
191
+
192
+ if (el.tagName === 'SELECT') {
193
+ if (required && !value) flags.valueMissing = true;
194
+ return flags;
195
+ }
196
+
197
+ if (required && value === '') flags.valueMissing = true;
198
+ if (value === '') return flags;
199
+
200
+ if (type === 'email' && !EMAIL_RE.test(value)) flags.typeMismatch = true;
201
+ if (type === 'url' && !URL_RE.test(value)) flags.typeMismatch = true;
202
+
203
+ var pattern = el.getAttribute('pattern');
204
+ if (pattern) {
205
+ try {
206
+ if (!new RegExp('^(?:' + pattern + ')$').test(value)) flags.patternMismatch = true;
207
+ } catch (e) {
208
+ // Invalid pattern attribute: browsers treat this as never matching,
209
+ // but silently ignoring it is the safer default for a headless sandbox.
210
+ }
211
+ }
212
+
213
+ var maxlength = el.getAttribute('maxlength');
214
+ if (maxlength !== null && value.length > parseInt(maxlength, 10)) flags.tooLong = true;
215
+
216
+ var minlength = el.getAttribute('minlength');
217
+ if (minlength !== null && value.length < parseInt(minlength, 10)) flags.tooShort = true;
218
+
219
+ if (type === 'number' || type === 'range') {
220
+ var num = Number(value);
221
+ if (value.trim() === '' || isNaN(num)) {
222
+ flags.badInput = true;
223
+ } else {
224
+ var min = el.getAttribute('min');
225
+ var max = el.getAttribute('max');
226
+ if (min !== null && num < Number(min)) flags.rangeUnderflow = true;
227
+ if (max !== null && num > Number(max)) flags.rangeOverflow = true;
228
+ }
229
+ }
230
+
231
+ return flags;
232
+ }
233
+
234
+ Object.defineProperty(Element.prototype, 'validity', {
235
+ configurable: true,
236
+ get: function() { return new ValidityState(computeValidityFlags(this)); },
237
+ });
238
+
239
+ Object.defineProperty(Element.prototype, 'willValidate', {
240
+ configurable: true,
241
+ get: function() {
242
+ return isFieldElement(this) && !this.hasAttribute('disabled') && fieldType(this) !== 'hidden';
243
+ },
244
+ });
245
+
246
+ var DEFAULT_MESSAGES = {
247
+ valueMissing: 'Please fill out this field.',
248
+ typeMismatch: 'Please enter a valid value.',
249
+ patternMismatch: 'Please match the requested format.',
250
+ tooLong: 'Please shorten this text.',
251
+ tooShort: 'Please lengthen this text.',
252
+ rangeUnderflow: 'Value must be greater than or equal to the minimum.',
253
+ rangeOverflow: 'Value must be less than or equal to the maximum.',
254
+ badInput: 'Please enter a valid value.',
255
+ };
256
+
257
+ Object.defineProperty(Element.prototype, 'validationMessage', {
258
+ configurable: true,
259
+ get: function() {
260
+ if (this._customValidity) return this._customValidity;
261
+ var flags = computeValidityFlags(this);
262
+ for (var i = 0; i < VALIDITY_KEYS.length; i++) {
263
+ if (flags[VALIDITY_KEYS[i]]) return DEFAULT_MESSAGES[VALIDITY_KEYS[i]] || '';
264
+ }
265
+ return '';
266
+ },
267
+ });
268
+
269
+ Element.prototype.setCustomValidity = function(message) {
270
+ this._customValidity = message == null ? '' : String(message);
271
+ };
272
+
273
+ function fieldCheckValidity(el) {
274
+ var valid = new ValidityState(computeValidityFlags(el)).valid;
275
+ if (!valid) {
276
+ var evt = new Event('invalid', { bubbles: false, cancelable: true });
277
+ el.dispatchEvent(evt);
278
+ }
279
+ return valid;
280
+ }
281
+
282
+ function formFields(form) {
283
+ return Array.prototype.filter.call(form.querySelectorAll('input, select, textarea'), function(el) {
284
+ return !el.hasAttribute('disabled');
285
+ });
286
+ }
287
+
288
+ Element.prototype.checkValidity = function() {
289
+ if (this.tagName === 'FORM') {
290
+ var fields = formFields(this);
291
+ var allValid = true;
292
+ for (var i = 0; i < fields.length; i++) {
293
+ if (!fieldCheckValidity(fields[i])) allValid = false;
294
+ }
295
+ return allValid;
296
+ }
297
+ return fieldCheckValidity(this);
298
+ };
299
+
300
+ Element.prototype.reportValidity = function() {
301
+ return this.checkValidity();
302
+ };
303
+
304
+ // ── element.form / form.elements ────────────────────────────────────────
305
+ var FORM_ASSOCIATED_TAGS = ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'FIELDSET', 'OUTPUT'];
306
+
307
+ function formControls(form) {
308
+ return Array.prototype.slice.call(form.querySelectorAll('input, select, textarea, button, fieldset, output'));
309
+ }
310
+
311
+ Object.defineProperty(Element.prototype, 'form', {
312
+ configurable: true,
313
+ get: function() {
314
+ if (FORM_ASSOCIATED_TAGS.indexOf(this.tagName) === -1) return undefined;
315
+ var formId = this.getAttribute('form');
316
+ if (formId) {
317
+ var byId = document.getElementById(formId);
318
+ if (byId && byId.tagName === 'FORM') return byId;
319
+ }
320
+ return this.closest('form');
321
+ },
322
+ });
323
+
324
+ Object.defineProperty(Element.prototype, 'elements', {
325
+ configurable: true,
326
+ get: function() {
327
+ if (this.tagName !== 'FORM') return undefined;
328
+ return formControls(this);
329
+ },
330
+ });
331
+
332
+ var LABELABLE_TAGS = ['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'];
333
+
334
+ Object.defineProperty(Element.prototype, 'labels', {
335
+ configurable: true,
336
+ get: function() {
337
+ if (LABELABLE_TAGS.indexOf(this.tagName) === -1) return null;
338
+ if (this.tagName === 'INPUT' && (this.getAttribute('type') || '').toLowerCase() === 'hidden') return null;
339
+ var result = [];
340
+ if (this.id) {
341
+ var candidates = this.ownerDocument.querySelectorAll('label[for]');
342
+ for (var i = 0; i < candidates.length; i++) {
343
+ if (candidates[i].getAttribute('for') === this.id) result.push(candidates[i]);
344
+ }
345
+ }
346
+ var wrapping = this.closest('label');
347
+ if (wrapping && result.indexOf(wrapping) === -1) result.push(wrapping);
348
+ return result;
349
+ },
350
+ });
351
+ })();
352
+ )JS";
353
+
354
+ } // namespace
355
+
356
+ void FormBindings::install(JSContext* ctx) {
357
+ JSValue result = JS_Eval(ctx, kFormBootstrapScript, strlen(kFormBootstrapScript),
358
+ "<form-bootstrap>", JS_EVAL_TYPE_GLOBAL);
359
+ if (JS_IsException(result)) {
360
+ JS_FreeValue(ctx, JS_GetException(ctx));
361
+ }
362
+ JS_FreeValue(ctx, result);
363
+ }
364
+
365
+ } // 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.FormData, Element.prototype.requestSubmit/submit, and
8
+ // the Constraint Validation API (globalThis.ValidityState,
9
+ // Element.prototype.validity/willValidate/validationMessage/checkValidity()/
10
+ // reportValidity()/setCustomValidity()). Tag-checked for "form"/"input"/
11
+ // "select"/"textarea" internally, mirroring ElementBindings' value/checked
12
+ // pattern. Pure JS on top of the already-exposed getAttribute/value/checked/
13
+ // querySelectorAll/Event primitives — no native code needed. Must run after
14
+ // ElementBindings (for globalThis.Element) and EventBindings (for
15
+ // globalThis.Event).
16
+ struct FormBindings {
17
+ static void install(JSContext* ctx);
18
+ };
19
+
20
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,104 @@
1
+ #include "LayoutStubBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kLayoutStubBootstrapScript = R"JS(
9
+ (function() {
10
+ function definePositiveInt(proto, name) {
11
+ Object.defineProperty(proto, name, { get: function() { return 0; }, configurable: true });
12
+ }
13
+
14
+ // ── offset*/client*/scroll* dimensions ────────────────────────────────
15
+ ['offsetWidth', 'offsetHeight', 'offsetTop', 'offsetLeft',
16
+ 'clientWidth', 'clientHeight', 'clientTop', 'clientLeft',
17
+ 'scrollWidth', 'scrollHeight'].forEach(function(name) {
18
+ definePositiveInt(Element.prototype, name);
19
+ });
20
+ Object.defineProperty(Element.prototype, 'offsetParent', {
21
+ get: function() { return null; },
22
+ configurable: true,
23
+ });
24
+
25
+ // scrollTop/scrollLeft are real per-element state (jsdom does the same) —
26
+ // nothing reads them back to affect layout, but round-tripping a value a
27
+ // script just set is more useful than hardcoding 0.
28
+ Object.defineProperty(Element.prototype, 'scrollTop', {
29
+ get: function() { return this._scrollTop || 0; },
30
+ set: function(v) { this._scrollTop = Number(v) || 0; },
31
+ configurable: true,
32
+ });
33
+ Object.defineProperty(Element.prototype, 'scrollLeft', {
34
+ get: function() { return this._scrollLeft || 0; },
35
+ set: function(v) { this._scrollLeft = Number(v) || 0; },
36
+ configurable: true,
37
+ });
38
+
39
+ // ── scrollIntoView / scrollTo / scrollBy / scroll ─────────────────────
40
+ Element.prototype.scrollIntoView = function() {};
41
+ Element.prototype.scrollTo = function() {};
42
+ Element.prototype.scrollBy = function() {};
43
+ Element.prototype.scroll = function() {};
44
+
45
+ globalThis.scrollX = 0;
46
+ globalThis.scrollY = 0;
47
+ globalThis.pageXOffset = 0;
48
+ globalThis.pageYOffset = 0;
49
+ globalThis.scrollTo = function() {};
50
+ globalThis.scrollBy = function() {};
51
+ globalThis.scroll = function() {};
52
+
53
+ // ── document.elementFromPoint / elementsFromPoint ─────────────────────
54
+ document.elementFromPoint = function() { return null; };
55
+ document.elementsFromPoint = function() { return []; };
56
+
57
+ // ── ResizeObserver / IntersectionObserver ─────────────────────────────
58
+ function ResizeObserver(callback) {
59
+ if (typeof callback !== 'function') {
60
+ throw new TypeError("Failed to construct 'ResizeObserver': parameter 1 is not a function.");
61
+ }
62
+ this._callback = callback;
63
+ }
64
+ ResizeObserver.prototype.observe = function() {};
65
+ ResizeObserver.prototype.unobserve = function() {};
66
+ ResizeObserver.prototype.disconnect = function() {};
67
+ globalThis.ResizeObserver = ResizeObserver;
68
+
69
+ function normalizeThreshold(threshold) {
70
+ if (threshold === undefined) return [0];
71
+ var arr = Array.isArray(threshold) ? threshold.slice() : [threshold];
72
+ return arr.length ? arr : [0];
73
+ }
74
+
75
+ function IntersectionObserver(callback, options) {
76
+ if (typeof callback !== 'function') {
77
+ throw new TypeError("Failed to construct 'IntersectionObserver': parameter 1 is not a function.");
78
+ }
79
+ options = options || {};
80
+ this._callback = callback;
81
+ this.root = options.root || null;
82
+ this.rootMargin = options.rootMargin || '0px';
83
+ this.thresholds = normalizeThreshold(options.threshold);
84
+ }
85
+ IntersectionObserver.prototype.observe = function() {};
86
+ IntersectionObserver.prototype.unobserve = function() {};
87
+ IntersectionObserver.prototype.disconnect = function() {};
88
+ IntersectionObserver.prototype.takeRecords = function() { return []; };
89
+ globalThis.IntersectionObserver = IntersectionObserver;
90
+ })();
91
+ )JS";
92
+
93
+ } // namespace
94
+
95
+ void LayoutStubBindings::install(JSContext* ctx) {
96
+ JSValue result = JS_Eval(ctx, kLayoutStubBootstrapScript, strlen(kLayoutStubBootstrapScript),
97
+ "<layout-stub-bootstrap>", JS_EVAL_TYPE_GLOBAL);
98
+ if (JS_IsException(result)) {
99
+ JS_FreeValue(ctx, JS_GetException(ctx));
100
+ }
101
+ JS_FreeValue(ctx, result);
102
+ }
103
+
104
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,28 @@
1
+ #pragma once
2
+
3
+ // No layout/rendering engine backs this sandbox, so every geometry- and
4
+ // scroll-related API here is an inert stub rather than a real computation —
5
+ // same stance as CSSOMBindings' getComputedStyle and ElementBindings'
6
+ // getBoundingClientRect (both already all-zero without a cascade/layout
7
+ // pass). This file covers the rest of that surface: scrollIntoView/scrollTo/
8
+ // scrollBy/scroll (no-ops), offset*/client*/scroll* dimensions (0, except
9
+ // scrollTop/scrollLeft which are real per-element state — jsdom does the
10
+ // same), document.elementFromPoint/elementsFromPoint (null/[]), and
11
+ // ResizeObserver/IntersectionObserver (constructible, observe()/unobserve()/
12
+ // disconnect() no-op, callback never fires). jsdom itself doesn't expose
13
+ // Resize/IntersectionObserver at all; this project adds them as defensive
14
+ // "don't throw ReferenceError" stubs instead, matching the precedent set by
15
+ // window.history/window.getSelection().
16
+ //
17
+ // Must run after ElementBindings (globalThis.Element) and DocumentBindings
18
+ // (globalThis.document).
19
+
20
+ #include "quickjs.h"
21
+
22
+ namespace margelo::nitro::nitrojsdom {
23
+
24
+ struct LayoutStubBindings {
25
+ static void install(JSContext* ctx);
26
+ };
27
+
28
+ } // namespace margelo::nitro::nitrojsdom