@salve-software/react-native-nitro-jsdom 2.0.0 → 2.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 (33) hide show
  1. package/android/CMakeLists.txt +2 -0
  2. package/cpp/HybridHtmlSandbox.cpp +7 -4
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +3 -3
  5. package/cpp/lexbor/LexborDocument.hpp +3 -2
  6. package/cpp/quickjs/DOMBindings.cpp +5 -0
  7. package/cpp/quickjs/QuickJSRuntime.hpp +1 -0
  8. package/cpp/quickjs/bindings/CSSOMBindings.cpp +50 -0
  9. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +1 -1
  10. package/cpp/quickjs/bindings/DocumentBindings.cpp +8 -0
  11. package/cpp/quickjs/bindings/ElementBindings.cpp +221 -1
  12. package/cpp/quickjs/bindings/EventBindings.cpp +8 -4
  13. package/cpp/quickjs/bindings/FormBindings.cpp +71 -0
  14. package/cpp/quickjs/bindings/IntlBindings.cpp +352 -0
  15. package/cpp/quickjs/bindings/IntlBindings.hpp +29 -0
  16. package/cpp/quickjs/bindings/UrlBindings.cpp +34 -0
  17. package/cpp/quickjs/bindings/WindowBindings.cpp +9 -0
  18. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp +241 -0
  19. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.hpp +30 -0
  20. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +2 -1
  21. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  22. package/lib/module/classes/JSDOM/JSDOM.class.js +2 -1
  23. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  24. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  25. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +5 -4
  26. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -1
  27. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  28. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  29. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  30. package/package.json +1 -1
  31. package/src/classes/JSDOM/JSDOM.class.ts +2 -1
  32. package/src/classes/JSDOM/types/IJSDOMOptions.ts +5 -4
  33. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -0,0 +1,352 @@
1
+ #include "IntlBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kIntlBootstrapScript = R"JS(
9
+ (function() {
10
+ var LOCALE_DATA = {
11
+ en: {
12
+ months: {
13
+ long: ['January','February','March','April','May','June','July','August','September','October','November','December'],
14
+ short: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
15
+ narrow: ['J','F','M','A','M','J','J','A','S','O','N','D'],
16
+ },
17
+ weekdays: {
18
+ long: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
19
+ short: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
20
+ narrow: ['S','M','T','W','T','F','S'],
21
+ },
22
+ dayPeriod: ['AM', 'PM'],
23
+ decimal: '.',
24
+ group: ',',
25
+ dateOrder: 'MDY',
26
+ currencySpace: false,
27
+ },
28
+ pt: {
29
+ months: {
30
+ long: ['janeiro','fevereiro','março','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro'],
31
+ short: ['jan','fev','mar','abr','mai','jun','jul','ago','set','out','nov','dez'],
32
+ narrow: ['J','F','M','A','M','J','J','A','S','O','N','D'],
33
+ },
34
+ weekdays: {
35
+ long: ['domingo','segunda-feira','terça-feira','quarta-feira','quinta-feira','sexta-feira','sábado'],
36
+ short: ['dom','seg','ter','qua','qui','sex','sáb'],
37
+ narrow: ['D','S','T','Q','Q','S','S'],
38
+ },
39
+ dayPeriod: ['AM', 'PM'],
40
+ decimal: ',',
41
+ group: '.',
42
+ dateOrder: 'DMY',
43
+ currencySpace: true,
44
+ },
45
+ };
46
+
47
+ var CURRENCY_SYMBOLS = {
48
+ USD: '$', EUR: '€', GBP: '£', JPY: '¥', BRL: 'R$', CAD: 'CA$', AUD: 'A$', CHF: 'CHF', CNY: 'CN¥', INR: '₹', MXN: 'MX$',
49
+ };
50
+
51
+ var CURRENCY_DECIMALS = {
52
+ JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, UGX: 0, XAF: 0, XOF: 0, XPF: 0,
53
+ BHD: 3, IQD: 3, JOD: 3, KWD: 3, OMR: 3, TND: 3,
54
+ };
55
+
56
+ function currencyDecimals(code) {
57
+ return CURRENCY_DECIMALS[code] !== undefined ? CURRENCY_DECIMALS[code] : 2;
58
+ }
59
+
60
+ function localeLanguage(locales) {
61
+ var tag = Array.isArray(locales) ? locales[0] : locales;
62
+ tag = String(tag || 'en');
63
+ return tag.split(/[-_]/)[0].toLowerCase();
64
+ }
65
+
66
+ function resolveLocale(locales) {
67
+ var lang = localeLanguage(locales);
68
+ return LOCALE_DATA[lang] || LOCALE_DATA.en;
69
+ }
70
+
71
+ function groupInteger(digits, groupSep) {
72
+ var out = '';
73
+ var count = 0;
74
+ for (var i = digits.length - 1; i >= 0; i--) {
75
+ out = digits.charAt(i) + out;
76
+ count++;
77
+ if (count % 3 === 0 && i !== 0) out = groupSep + out;
78
+ }
79
+ return out;
80
+ }
81
+
82
+ function formatFixed(value, minFrac, maxFrac, useGrouping, data) {
83
+ var negative = value < 0;
84
+ var abs = Math.abs(value);
85
+ var fixed = abs.toFixed(maxFrac);
86
+ var parts = fixed.split('.');
87
+ var intPart = parts[0];
88
+ var fracPart = parts[1] || '';
89
+
90
+ while (fracPart.length > minFrac && fracPart.charAt(fracPart.length - 1) === '0') {
91
+ fracPart = fracPart.slice(0, -1);
92
+ }
93
+
94
+ var out = useGrouping ? groupInteger(intPart, data.group) : intPart;
95
+ if (fracPart.length > 0) out += data.decimal + fracPart;
96
+ return (negative ? '-' : '') + out;
97
+ }
98
+
99
+ function NumberFormat(locales, options) {
100
+ this._data = resolveLocale(locales);
101
+ options = options || {};
102
+ this._style = options.style || 'decimal';
103
+ if (this._style === 'currency') {
104
+ if (!options.currency) throw new TypeError('Currency code is required with currency style.');
105
+ this._currency = options.currency;
106
+ }
107
+ var currDecimals = this._style === 'currency' ? currencyDecimals(this._currency) : undefined;
108
+ var defaultMinFrac = this._style === 'currency' ? currDecimals : 0;
109
+ var defaultMaxFrac = this._style === 'currency' ? currDecimals : (this._style === 'percent' ? 0 : 3);
110
+ this._minFrac = options.minimumFractionDigits !== undefined ? options.minimumFractionDigits : defaultMinFrac;
111
+ this._maxFrac = options.maximumFractionDigits !== undefined ? options.maximumFractionDigits : Math.max(defaultMaxFrac, this._minFrac);
112
+ this._useGrouping = options.useGrouping !== undefined ? !!options.useGrouping : true;
113
+ }
114
+
115
+ NumberFormat.prototype.format = function(value) {
116
+ value = Number(value);
117
+ if (this._style === 'percent') value = value * 100;
118
+ var formatted = formatFixed(value, this._minFrac, this._maxFrac, this._useGrouping, this._data);
119
+ if (this._style === 'percent') return formatted + '%';
120
+ if (this._style === 'currency') {
121
+ var symbol = CURRENCY_SYMBOLS[this._currency] || this._currency;
122
+ return symbol + (this._data.currencySpace ? ' ' : '') + formatted;
123
+ }
124
+ return formatted;
125
+ };
126
+
127
+ NumberFormat.prototype.resolvedOptions = function() {
128
+ return {
129
+ style: this._style,
130
+ currency: this._style === 'currency' ? this._currency : undefined,
131
+ minimumFractionDigits: this._minFrac,
132
+ maximumFractionDigits: this._maxFrac,
133
+ useGrouping: this._useGrouping,
134
+ };
135
+ };
136
+
137
+ function pad2(n) { return (n < 10 ? '0' : '') + n; }
138
+
139
+ var DATE_STYLE_OPTIONS = {
140
+ short: { year: '2-digit', month: 'numeric', day: 'numeric' },
141
+ medium: { year: 'numeric', month: 'short', day: 'numeric' },
142
+ long: { year: 'numeric', month: 'long', day: 'numeric' },
143
+ full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
144
+ };
145
+ var TIME_STYLE_OPTIONS = {
146
+ short: { hour: 'numeric', minute: 'numeric' },
147
+ medium: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
148
+ long: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
149
+ full: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
150
+ };
151
+
152
+ function DateTimeFormat(locales, options) {
153
+ this._data = resolveLocale(locales);
154
+ options = options || {};
155
+
156
+ if (options.dateStyle || options.timeStyle) {
157
+ var withDate = options.dateStyle ? (DATE_STYLE_OPTIONS[options.dateStyle] || DATE_STYLE_OPTIONS.medium) : {};
158
+ var withTime = options.timeStyle ? (TIME_STYLE_OPTIONS[options.timeStyle] || TIME_STYLE_OPTIONS.short) : {};
159
+ options = Object.assign({}, withDate, withTime, options);
160
+ } else if (Object.keys(options).length === 0) {
161
+ options = { year: 'numeric', month: 'numeric', day: 'numeric' };
162
+ }
163
+
164
+ this._options = options;
165
+ }
166
+
167
+ function datePartFor(date, o, data) {
168
+ var textual = o.month === 'long' || o.month === 'short' || o.month === 'narrow';
169
+ var day = o.day ? (o.day === '2-digit' ? pad2(date.getDate()) : String(date.getDate())) : '';
170
+ var year = o.year ? (o.year === '2-digit' ? pad2(date.getFullYear() % 100) : String(date.getFullYear())) : '';
171
+ var month = o.month
172
+ ? (textual ? data.months[o.month][date.getMonth()] : (o.month === '2-digit' ? pad2(date.getMonth() + 1) : String(date.getMonth() + 1)))
173
+ : '';
174
+
175
+ if (!month && !day && !year) return '';
176
+
177
+ if (textual) {
178
+ if (data.dateOrder === 'MDY') {
179
+ var head = day ? month + ' ' + day : month;
180
+ return year ? head + ', ' + year : head;
181
+ }
182
+ return [day, month, year].filter(Boolean).join(' ');
183
+ }
184
+
185
+ var ordered = data.dateOrder === 'MDY' ? [month, day, year] : [day, month, year];
186
+ return ordered.filter(Boolean).join('/');
187
+ }
188
+
189
+ DateTimeFormat.prototype.format = function(date) {
190
+ date = date === undefined ? new Date() : (date instanceof Date ? date : new Date(date));
191
+ var o = this._options;
192
+ var data = this._data;
193
+ var pieces = [];
194
+
195
+ var datePart = datePartFor(date, o, data);
196
+ if (o.weekday) {
197
+ var w = date.getDay();
198
+ var weekdayName = data.weekdays[o.weekday === 'narrow' ? 'narrow' : (o.weekday === 'long' ? 'long' : 'short')][w];
199
+ pieces.push(datePart ? weekdayName + ', ' + datePart : weekdayName);
200
+ } else if (datePart) {
201
+ pieces.push(datePart);
202
+ }
203
+
204
+ if (o.hour) {
205
+ var h = date.getHours();
206
+ var hour12 = o.hour12 !== false;
207
+ var hourVal = hour12 ? (h % 12 === 0 ? 12 : h % 12) : h;
208
+ var timeSegs = [o.hour === '2-digit' ? pad2(hourVal) : String(hourVal)];
209
+ if (o.minute) timeSegs.push(pad2(date.getMinutes()));
210
+ if (o.second) timeSegs.push(pad2(date.getSeconds()));
211
+ var timeStr = timeSegs.join(':');
212
+ if (hour12) timeStr += ' ' + (h < 12 ? data.dayPeriod[0] : data.dayPeriod[1]);
213
+ pieces.push(timeStr);
214
+ } else if (o.minute) {
215
+ var timeSegs2 = [pad2(date.getMinutes())];
216
+ if (o.second) timeSegs2.push(pad2(date.getSeconds()));
217
+ pieces.push(timeSegs2.join(':'));
218
+ }
219
+
220
+ return pieces.join(', ');
221
+ };
222
+
223
+ DateTimeFormat.prototype.resolvedOptions = function() {
224
+ return Object.assign({}, this._options);
225
+ };
226
+
227
+ function pluralCategoryFor(n, lang) {
228
+ n = Math.abs(Number(n));
229
+ if (lang === 'pt') return (n === 0 || n === 1) ? 'one' : 'other';
230
+ return n === 1 ? 'one' : 'other';
231
+ }
232
+
233
+ function PluralRules(locales, options) {
234
+ this._lang = localeLanguage(locales);
235
+ options = options || {};
236
+ this._type = options.type || 'cardinal';
237
+ }
238
+
239
+ PluralRules.prototype.select = function(n) {
240
+ return pluralCategoryFor(n, this._lang);
241
+ };
242
+
243
+ PluralRules.prototype.resolvedOptions = function() {
244
+ return { locale: this._lang, type: this._type, pluralCategories: ['one', 'other'] };
245
+ };
246
+
247
+ var RTF_UNITS = {
248
+ en: {
249
+ year: ['year', 'years'], quarter: ['quarter', 'quarters'], month: ['month', 'months'],
250
+ week: ['week', 'weeks'], day: ['day', 'days'], hour: ['hour', 'hours'],
251
+ minute: ['minute', 'minutes'], second: ['second', 'seconds'],
252
+ },
253
+ pt: {
254
+ year: ['ano', 'anos'], quarter: ['trimestre', 'trimestres'], month: ['mês', 'meses'],
255
+ week: ['semana', 'semanas'], day: ['dia', 'dias'], hour: ['hora', 'horas'],
256
+ minute: ['minuto', 'minutos'], second: ['segundo', 'segundos'],
257
+ },
258
+ };
259
+
260
+ var RTF_PATTERNS = {
261
+ en: { past: '{0} {1} ago', future: 'in {0} {1}' },
262
+ pt: { past: 'há {0} {1}', future: 'em {0} {1}' },
263
+ };
264
+
265
+ var RTF_AUTO = {
266
+ en: {
267
+ day: { '-1': 'yesterday', '0': 'today', '1': 'tomorrow' },
268
+ week: { '-1': 'last week', '0': 'this week', '1': 'next week' },
269
+ month: { '-1': 'last month', '0': 'this month', '1': 'next month' },
270
+ year: { '-1': 'last year', '0': 'this year', '1': 'next year' },
271
+ second: { '0': 'now' },
272
+ },
273
+ pt: {
274
+ day: { '-1': 'ontem', '0': 'hoje', '1': 'amanhã' },
275
+ week: { '-1': 'semana passada', '0': 'esta semana', '1': 'semana que vem' },
276
+ month: { '-1': 'mês passado', '0': 'este mês', '1': 'próximo mês' },
277
+ year: { '-1': 'ano passado', '0': 'este ano', '1': 'próximo ano' },
278
+ second: { '0': 'agora' },
279
+ },
280
+ };
281
+
282
+ var RTF_UNIT_ALIASES = {
283
+ years: 'year', quarters: 'quarter', months: 'month', weeks: 'week',
284
+ days: 'day', hours: 'hour', minutes: 'minute', seconds: 'second',
285
+ };
286
+
287
+ function RelativeTimeFormat(locales, options) {
288
+ this._lang = RTF_UNITS[localeLanguage(locales)] ? localeLanguage(locales) : 'en';
289
+ options = options || {};
290
+ this._numeric = options.numeric || 'always';
291
+ this._style = options.style || 'long';
292
+ }
293
+
294
+ RelativeTimeFormat.prototype.format = function(value, unit) {
295
+ value = Number(value);
296
+ unit = RTF_UNIT_ALIASES[unit] || unit;
297
+ var units = RTF_UNITS[this._lang][unit];
298
+ if (!units) throw new RangeError('Invalid unit argument for RelativeTimeFormat.format()');
299
+
300
+ if (this._numeric === 'auto') {
301
+ var autoData = RTF_AUTO[this._lang][unit];
302
+ if (autoData && autoData[String(value)] !== undefined) {
303
+ return autoData[String(value)];
304
+ }
305
+ }
306
+
307
+ var n = Math.abs(value);
308
+ var unitName = units[pluralCategoryFor(n, this._lang) === 'one' ? 0 : 1];
309
+ var pattern = value < 0 ? RTF_PATTERNS[this._lang].past : RTF_PATTERNS[this._lang].future;
310
+ return pattern.replace('{0}', String(n)).replace('{1}', unitName);
311
+ };
312
+
313
+ RelativeTimeFormat.prototype.resolvedOptions = function() {
314
+ return { locale: this._lang, style: this._style, numeric: this._numeric };
315
+ };
316
+
317
+ globalThis.Intl = globalThis.Intl || {};
318
+ globalThis.Intl.NumberFormat = NumberFormat;
319
+ globalThis.Intl.DateTimeFormat = DateTimeFormat;
320
+ globalThis.Intl.PluralRules = PluralRules;
321
+ globalThis.Intl.RelativeTimeFormat = RelativeTimeFormat;
322
+
323
+ function hasOwnKeys(o) { return !!o && Object.keys(o).length > 0; }
324
+
325
+ Number.prototype.toLocaleString = function(locales, options) {
326
+ return new NumberFormat(locales, options).format(this);
327
+ };
328
+
329
+ Date.prototype.toLocaleString = function(locales, options) {
330
+ return new DateTimeFormat(locales, hasOwnKeys(options) ? options : { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(this);
331
+ };
332
+ Date.prototype.toLocaleDateString = function(locales, options) {
333
+ return new DateTimeFormat(locales, hasOwnKeys(options) ? options : { year: 'numeric', month: 'numeric', day: 'numeric' }).format(this);
334
+ };
335
+ Date.prototype.toLocaleTimeString = function(locales, options) {
336
+ return new DateTimeFormat(locales, hasOwnKeys(options) ? options : { hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(this);
337
+ };
338
+ })();
339
+ )JS";
340
+
341
+ } // namespace
342
+
343
+ void IntlBindings::install(JSContext* ctx) {
344
+ JSValue result = JS_Eval(ctx, kIntlBootstrapScript, strlen(kIntlBootstrapScript),
345
+ "<intl-bootstrap>", JS_EVAL_TYPE_GLOBAL);
346
+ if (JS_IsException(result)) {
347
+ JS_FreeValue(ctx, JS_GetException(ctx));
348
+ }
349
+ JS_FreeValue(ctx, result);
350
+ }
351
+
352
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,29 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers a pure-JS globalThis.Intl.NumberFormat/DateTimeFormat/
8
+ // PluralRules/RelativeTimeFormat (no ICU — QuickJS ships none). Locale data
9
+ // is a small hand-built table (en + pt, the two this project's users
10
+ // actually need), not real CLDR data. Also rewires Number.prototype.
11
+ // toLocaleString and Date.prototype.toLocaleString/toLocaleDateString/
12
+ // toLocaleTimeString to go through these instead of QuickJS's own
13
+ // locale-blind built-ins.
14
+ //
15
+ // RelativeTimeFormat's `style` option ('long'/'short'/'narrow') is accepted
16
+ // but 'short'/'narrow' render identically to 'long' — there's no abbreviated
17
+ // unit-name table, same trade-off DateTimeFormat already made collapsing
18
+ // timeStyle 'long'/'full'. `numeric: 'auto'` only has special phrasing
19
+ // ("yesterday", "next week", ..., plus "now"/"agora" for second: 0) for
20
+ // day/week/month/year/second — hour/minute/quarter always render
21
+ // numerically, matching real Intl's own behavior for units without a
22
+ // natural auto phrase. Auto phrasing only ever applies to exact supported
23
+ // integer values (e.g. -1/0/1) — a fractional value like 1.2 always falls
24
+ // through to numeric formatting rather than rounding into a phrase.
25
+ struct IntlBindings {
26
+ static void install(JSContext* ctx);
27
+ };
28
+
29
+ } // namespace margelo::nitro::nitrojsdom
@@ -278,6 +278,40 @@ const char* kUrlBootstrapScript = R"JS(
278
278
  };
279
279
 
280
280
  globalThis.URL = URL;
281
+
282
+ function isAnchorElement(el) {
283
+ return !!el && (el.tagName === 'A' || el.tagName === 'AREA');
284
+ }
285
+
286
+ function anchorUrl(el) {
287
+ var raw = el.getAttribute('href');
288
+ if (raw === null) return null;
289
+ try { return new URL(raw, document.baseURI); } catch (e) { return null; }
290
+ }
291
+
292
+ Object.defineProperty(Element.prototype, 'href', {
293
+ configurable: true,
294
+ get: function() {
295
+ if (!isAnchorElement(this)) return undefined;
296
+ var u = anchorUrl(this);
297
+ return u ? u.href : '';
298
+ },
299
+ set: function(value) {
300
+ if (!isAnchorElement(this)) return;
301
+ this.setAttribute('href', String(value));
302
+ },
303
+ });
304
+
305
+ ['protocol', 'username', 'password', 'hostname', 'port', 'pathname', 'search', 'hash', 'host', 'origin'].forEach(function(part) {
306
+ Object.defineProperty(Element.prototype, part, {
307
+ configurable: true,
308
+ get: function() {
309
+ if (!isAnchorElement(this)) return undefined;
310
+ var u = anchorUrl(this);
311
+ return u ? u[part] : '';
312
+ },
313
+ });
314
+ });
281
315
  })();
282
316
  )JS";
283
317
 
@@ -354,6 +354,15 @@ const char* kLocationBootstrapScript = R"JS(
354
354
  globalThis.Location = Location;
355
355
  globalThis.location = new Location(globalThis.__initialHref);
356
356
  delete globalThis.__initialHref;
357
+
358
+ if (typeof document !== 'undefined') {
359
+ Object.defineProperty(document, 'location', {
360
+ get: function() { return location; },
361
+ set: function(v) { location.href = v; },
362
+ enumerable: true,
363
+ configurable: true,
364
+ });
365
+ }
357
366
  })();
358
367
  )JS";
359
368
 
@@ -0,0 +1,241 @@
1
+ #include "WindowNamedPropertiesBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kWindowNamedPropertiesBootstrapScript = R"JS(
9
+ (function() {
10
+ var NAME_ATTR_TAGS = { embed: 1, form: 1, img: 1, object: 1, iframe: 1, frame: 1 };
11
+
12
+ // registry: key -> element[] currently claiming it. owned: key -> true once
13
+ // this module has written globalThis[key] (so it knows it's safe to delete
14
+ // or reassign later without clobbering something else's global).
15
+ var registry = Object.create(null);
16
+ var owned = Object.create(null);
17
+
18
+ function isConnected(el) {
19
+ var docEl = document.documentElement;
20
+ var n = el;
21
+ while (n) {
22
+ if (n === docEl) return true;
23
+ n = n.parentNode;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ function refreshKey(key) {
29
+ var els = registry[key];
30
+ if (!els || els.length === 0) {
31
+ if (owned[key]) { delete globalThis[key]; delete owned[key]; }
32
+ return;
33
+ }
34
+ if (!owned[key] && (key in globalThis)) return; // don't clobber a real global
35
+ globalThis[key] = els.length === 1 ? els[0] : els.slice();
36
+ owned[key] = true;
37
+ }
38
+
39
+ function addToRegistry(key, el) {
40
+ if (!key) return;
41
+ var els = registry[key] || (registry[key] = []);
42
+ if (els.indexOf(el) === -1) els.push(el);
43
+ refreshKey(key);
44
+ }
45
+
46
+ function removeFromRegistry(key, el) {
47
+ if (!key) return;
48
+ var els = registry[key];
49
+ if (!els) return;
50
+ var idx = els.indexOf(el);
51
+ if (idx !== -1) els.splice(idx, 1);
52
+ refreshKey(key);
53
+ }
54
+
55
+ function keysFor(el) {
56
+ var keys = [];
57
+ var id = el.getAttribute ? el.getAttribute('id') : null;
58
+ if (id) keys.push(id);
59
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
60
+ if (NAME_ATTR_TAGS[tag]) {
61
+ var name = el.getAttribute('name');
62
+ if (name) keys.push(name);
63
+ }
64
+ return keys;
65
+ }
66
+
67
+ function registerIfConnected(el) {
68
+ if (!el || el.nodeType !== 1 || !isConnected(el)) return;
69
+ keysFor(el).forEach(function(k) { addToRegistry(k, el); });
70
+ }
71
+
72
+ function unregister(el) {
73
+ if (!el || el.nodeType !== 1) return;
74
+ keysFor(el).forEach(function(k) { removeFromRegistry(k, el); });
75
+ }
76
+
77
+ function registerSubtree(root) {
78
+ if (!root) return;
79
+ if (root.nodeType === 1) registerIfConnected(root);
80
+ if (typeof root.querySelectorAll !== 'function') return;
81
+ var found = root.querySelectorAll('*');
82
+ for (var i = 0; i < found.length; i++) registerIfConnected(found[i]);
83
+ }
84
+
85
+ function unregisterSubtree(root) {
86
+ if (!root) return;
87
+ if (root.nodeType === 1) unregister(root);
88
+ if (typeof root.querySelectorAll !== 'function') return;
89
+ var found = root.querySelectorAll('*');
90
+ for (var i = 0; i < found.length; i++) unregister(found[i]);
91
+ }
92
+
93
+ function collectSubtreeKeys(root) {
94
+ var pending = [];
95
+ function collect(el) {
96
+ if (!el || el.nodeType !== 1) return;
97
+ keysFor(el).forEach(function(k) { pending.push({ key: k, el: el }); });
98
+ }
99
+ if (!root) return pending;
100
+ collect(root);
101
+ if (typeof root.querySelectorAll === 'function') {
102
+ var found = root.querySelectorAll('*');
103
+ for (var i = 0; i < found.length; i++) collect(found[i]);
104
+ }
105
+ return pending;
106
+ }
107
+
108
+ function commitUnregister(pending) {
109
+ pending.forEach(function(entry) { removeFromRegistry(entry.key, entry.el); });
110
+ }
111
+
112
+ // ── Initial population: elements already present in the parsed document ──
113
+ if (document.documentElement) registerSubtree(document.documentElement);
114
+
115
+ // ── appendChild / removeChild ─────────────────────────────────────────
116
+ var DOCUMENT_FRAGMENT_NODE = 11;
117
+
118
+ var origAppendChild = Node.prototype.appendChild;
119
+ Node.prototype.appendChild = function(child) {
120
+ var moved = (child && child.nodeType === DOCUMENT_FRAGMENT_NODE)
121
+ ? Array.prototype.slice.call(child.childNodes)
122
+ : [child];
123
+ var result = origAppendChild.call(this, child);
124
+ moved.forEach(registerSubtree);
125
+ return result;
126
+ };
127
+
128
+ var origRemoveChild = Node.prototype.removeChild;
129
+ Node.prototype.removeChild = function(child) {
130
+ var pending = collectSubtreeKeys(child);
131
+ var willRemove = child && child.parentNode === this;
132
+ var result = origRemoveChild.call(this, child);
133
+ if (willRemove) commitUnregister(pending);
134
+ return result;
135
+ };
136
+
137
+ var origRemove = Element.prototype.remove;
138
+ Element.prototype.remove = function() {
139
+ var pending = collectSubtreeKeys(this);
140
+ var hadParent = this.parentNode !== null;
141
+ var result = origRemove.call(this);
142
+ if (hadParent) commitUnregister(pending);
143
+ return result;
144
+ };
145
+
146
+ // ── innerHTML ────────────────────────────────────────────────────────
147
+ var innerHTMLDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
148
+ if (innerHTMLDesc && innerHTMLDesc.set) {
149
+ var origInnerHTMLSet = innerHTMLDesc.set;
150
+ Object.defineProperty(Element.prototype, 'innerHTML', {
151
+ get: innerHTMLDesc.get,
152
+ set: function(html) {
153
+ unregisterSubtree(this); // walk the OLD content before it's replaced
154
+ origInnerHTMLSet.call(this, html);
155
+ registerSubtree(this); // register the NEW content
156
+ },
157
+ enumerable: innerHTMLDesc.enumerable,
158
+ configurable: true,
159
+ });
160
+ }
161
+
162
+ var textContentDesc = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
163
+ if (textContentDesc && textContentDesc.set) {
164
+ var origTextContentSet = textContentDesc.set;
165
+ Object.defineProperty(Node.prototype, 'textContent', {
166
+ get: textContentDesc.get,
167
+ set: function(value) {
168
+ unregisterSubtree(this);
169
+ origTextContentSet.call(this, value);
170
+ },
171
+ enumerable: textContentDesc.enumerable,
172
+ configurable: true,
173
+ });
174
+ }
175
+
176
+ // ── insertAdjacentHTML ──────────────────────────────────────────────
177
+ var origInsertAdjacentHTML = Element.prototype.insertAdjacentHTML;
178
+ Element.prototype.insertAdjacentHTML = function(position, html) {
179
+ origInsertAdjacentHTML.call(this, position, html);
180
+ var pos = String(position).toLowerCase();
181
+ registerSubtree((pos === 'beforebegin' || pos === 'afterend') ? (this.parentNode || this) : this);
182
+ };
183
+
184
+ // ── element.id (native accessor property — bypasses setAttribute) ──
185
+ var idDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'id');
186
+ if (idDesc && idDesc.set) {
187
+ var origIdSet = idDesc.set;
188
+ Object.defineProperty(Element.prototype, 'id', {
189
+ get: idDesc.get,
190
+ set: function(value) {
191
+ var oldValue = this.getAttribute('id');
192
+ origIdSet.call(this, value);
193
+ if (oldValue) removeFromRegistry(oldValue, this);
194
+ registerIfConnected(this);
195
+ },
196
+ enumerable: idDesc.enumerable,
197
+ configurable: true,
198
+ });
199
+ }
200
+
201
+ // ── setAttribute / removeAttribute (id/name changes) ───────────────
202
+ function isTrackedAttr(el, name) {
203
+ var lower = String(name).toLowerCase();
204
+ if (lower === 'id') return true;
205
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
206
+ return lower === 'name' && !!NAME_ATTR_TAGS[tag];
207
+ }
208
+
209
+ var origSetAttribute = Element.prototype.setAttribute;
210
+ Element.prototype.setAttribute = function(name, value) {
211
+ var tracked = isTrackedAttr(this, name);
212
+ var oldValue = tracked ? this.getAttribute(name) : null;
213
+ origSetAttribute.call(this, name, value);
214
+ if (tracked) {
215
+ if (oldValue) removeFromRegistry(oldValue, this);
216
+ registerIfConnected(this);
217
+ }
218
+ };
219
+
220
+ var origRemoveAttribute = Element.prototype.removeAttribute;
221
+ Element.prototype.removeAttribute = function(name) {
222
+ var tracked = isTrackedAttr(this, name);
223
+ var oldValue = tracked ? this.getAttribute(name) : null;
224
+ origRemoveAttribute.call(this, name);
225
+ if (tracked && oldValue) removeFromRegistry(oldValue, this);
226
+ };
227
+ })();
228
+ )JS";
229
+
230
+ } // namespace
231
+
232
+ void WindowNamedPropertiesBindings::install(JSContext* ctx) {
233
+ JSValue result = JS_Eval(ctx, kWindowNamedPropertiesBootstrapScript, strlen(kWindowNamedPropertiesBootstrapScript),
234
+ "<window-named-properties-bootstrap>", JS_EVAL_TYPE_GLOBAL);
235
+ if (JS_IsException(result)) {
236
+ JS_FreeValue(ctx, JS_GetException(ctx));
237
+ }
238
+ JS_FreeValue(ctx, result);
239
+ }
240
+
241
+ } // namespace margelo::nitro::nitrojsdom