@salve-software/react-native-nitro-jsdom 1.1.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.
- package/android/CMakeLists.txt +6 -0
- package/cpp/HybridHtmlSandbox.cpp +14 -7
- package/cpp/HybridHtmlSandbox.hpp +1 -1
- package/cpp/lexbor/LexborDocument.cpp +65 -3
- package/cpp/lexbor/LexborDocument.hpp +17 -2
- package/cpp/quickjs/DOMBindings.cpp +14 -1
- package/cpp/quickjs/DOMBindingsInternal.cpp +67 -0
- package/cpp/quickjs/DOMBindingsInternal.hpp +39 -0
- package/cpp/quickjs/QuickJSRuntime.cpp +13 -0
- package/cpp/quickjs/QuickJSRuntime.hpp +29 -0
- package/cpp/quickjs/bindings/AbortBindings.cpp +14 -0
- package/cpp/quickjs/bindings/BlobBindings.cpp +28 -1
- package/cpp/quickjs/bindings/BlobBindings.hpp +3 -3
- package/cpp/quickjs/bindings/CSSOMBindings.cpp +96 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
- package/cpp/quickjs/bindings/CustomElementsBindings.cpp +1 -1
- package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
- package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +174 -11
- package/cpp/quickjs/bindings/ElementBindings.cpp +516 -20
- package/cpp/quickjs/bindings/EventBindings.cpp +281 -5
- package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
- package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
- package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
- package/cpp/quickjs/bindings/FetchBindings.cpp +43 -8
- package/cpp/quickjs/bindings/FormBindings.cpp +294 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
- package/cpp/quickjs/bindings/IntlBindings.cpp +352 -0
- package/cpp/quickjs/bindings/IntlBindings.hpp +29 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +45 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +79 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
- package/cpp/quickjs/bindings/UrlBindings.cpp +43 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +295 -4
- package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
- package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp +241 -0
- package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.hpp +30 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +2 -1
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js +2 -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/classes/JSDOM/types/IJSDOMOptions.d.ts +5 -4
- package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.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 +17 -2
- package/src/classes/JSDOM/JSDOM.class.ts +2 -1
- package/src/classes/JSDOM/types/IJSDOMOptions.ts +5 -4
- package/src/specs/HtmlSandbox.nitro.ts +1 -1
|
@@ -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
|
|
@@ -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
|
|
@@ -138,6 +138,51 @@ const char* kIteratorBootstrapScript = R"JS(
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
};
|
|
141
|
+
|
|
142
|
+
proto.forEach = function(callback, thisArg) {
|
|
143
|
+
for (var i = 0; i < this.length; i++) callback.call(thisArg, this[i], i, this);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function arrayLikeIterator(self, mapEntry) {
|
|
147
|
+
var i = 0;
|
|
148
|
+
return {
|
|
149
|
+
next: function() {
|
|
150
|
+
if (i >= self.length) return { value: undefined, done: true };
|
|
151
|
+
var value = mapEntry(i, self[i]);
|
|
152
|
+
i++;
|
|
153
|
+
return { value: value, done: false };
|
|
154
|
+
},
|
|
155
|
+
[Symbol.iterator]: function() { return this; },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
proto.entries = function() {
|
|
160
|
+
return arrayLikeIterator(this, function(i, v) { return [i, v]; });
|
|
161
|
+
};
|
|
162
|
+
proto.keys = function() {
|
|
163
|
+
return arrayLikeIterator(this, function(i) { return i; });
|
|
164
|
+
};
|
|
165
|
+
proto.values = function() {
|
|
166
|
+
return arrayLikeIterator(this, function(i, v) { return v; });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
proto.item = function(index) {
|
|
170
|
+
return index >= 0 && index < this.length ? this[index] : null;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// namedItem() is spec'd on HTMLCollection only (matches by id, then by
|
|
174
|
+
// name), not NodeList — but both share this one LiveCollection class (see
|
|
175
|
+
// the forEach() comment above), so this stays a no-op-safe lookup for
|
|
176
|
+
// NodeLists of non-Elements (text/comment nodes have no getAttribute).
|
|
177
|
+
proto.namedItem = function(name) {
|
|
178
|
+
for (var i = 0; i < this.length; i++) {
|
|
179
|
+
var item = this[i];
|
|
180
|
+
if (typeof item.getAttribute !== 'function') continue;
|
|
181
|
+
if (item.getAttribute('id') === name || item.getAttribute('name') === name) return item;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
185
|
+
|
|
141
186
|
delete globalThis.__LiveCollectionProto;
|
|
142
187
|
})();
|
|
143
188
|
)JS";
|
|
@@ -107,6 +107,85 @@ const char* kTimerBootstrapScript = R"JS(
|
|
|
107
107
|
globalThis.queueMicrotask = function(callback) {
|
|
108
108
|
Promise.resolve().then(function() { callback(); });
|
|
109
109
|
};
|
|
110
|
+
|
|
111
|
+
globalThis.requestIdleCallback = function(callback, options) {
|
|
112
|
+
var timeout = options && options.timeout;
|
|
113
|
+
return setTimeout(function() {
|
|
114
|
+
callback({
|
|
115
|
+
didTimeout: false,
|
|
116
|
+
timeRemaining: function() { return 50; },
|
|
117
|
+
});
|
|
118
|
+
}, typeof timeout === 'number' ? Math.min(timeout, 1) : 1);
|
|
119
|
+
};
|
|
120
|
+
globalThis.cancelIdleCallback = function(id) {
|
|
121
|
+
clearTimeout(id);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
var __perfEntries = [];
|
|
125
|
+
function __perfFindMark(name) {
|
|
126
|
+
for (var i = __perfEntries.length - 1; i >= 0; i--) {
|
|
127
|
+
if (__perfEntries[i].name === name && __perfEntries[i].entryType === 'mark') return __perfEntries[i];
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function __perfRequireMark(name) {
|
|
132
|
+
var entry = __perfFindMark(name);
|
|
133
|
+
if (!entry) {
|
|
134
|
+
throw new DOMException(
|
|
135
|
+
"Failed to execute 'measure' on 'Performance': The mark '" + name + "' does not exist.", 'SyntaxError');
|
|
136
|
+
}
|
|
137
|
+
return entry.startTime;
|
|
138
|
+
}
|
|
139
|
+
performance.mark = function(name, options) {
|
|
140
|
+
var entry = {
|
|
141
|
+
name: String(name),
|
|
142
|
+
entryType: 'mark',
|
|
143
|
+
startTime: (options && options.startTime !== undefined) ? options.startTime : performance.now(),
|
|
144
|
+
duration: 0,
|
|
145
|
+
detail: (options && options.detail !== undefined) ? options.detail : null,
|
|
146
|
+
};
|
|
147
|
+
__perfEntries.push(entry);
|
|
148
|
+
return entry;
|
|
149
|
+
};
|
|
150
|
+
performance.measure = function(name, startOrOptions, endMark) {
|
|
151
|
+
var startTime = 0;
|
|
152
|
+
var endTime = performance.now();
|
|
153
|
+
var detail = null;
|
|
154
|
+
if (typeof startOrOptions === 'string') {
|
|
155
|
+
startTime = __perfRequireMark(startOrOptions);
|
|
156
|
+
if (endMark !== undefined) endTime = __perfRequireMark(endMark);
|
|
157
|
+
} else if (startOrOptions && typeof startOrOptions === 'object') {
|
|
158
|
+
if (startOrOptions.detail !== undefined) detail = startOrOptions.detail;
|
|
159
|
+
if (startOrOptions.start !== undefined) {
|
|
160
|
+
startTime = typeof startOrOptions.start === 'string' ? __perfRequireMark(startOrOptions.start) : startOrOptions.start;
|
|
161
|
+
}
|
|
162
|
+
if (startOrOptions.end !== undefined) {
|
|
163
|
+
endTime = typeof startOrOptions.end === 'string' ? __perfRequireMark(startOrOptions.end) : startOrOptions.end;
|
|
164
|
+
} else if (startOrOptions.duration !== undefined) {
|
|
165
|
+
endTime = startTime + startOrOptions.duration;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var entry = { name: String(name), entryType: 'measure', startTime: startTime, duration: endTime - startTime, detail: detail };
|
|
169
|
+
__perfEntries.push(entry);
|
|
170
|
+
return entry;
|
|
171
|
+
};
|
|
172
|
+
performance.getEntries = function() { return __perfEntries.slice(); };
|
|
173
|
+
performance.getEntriesByType = function(type) {
|
|
174
|
+
return __perfEntries.filter(function(e) { return e.entryType === type; });
|
|
175
|
+
};
|
|
176
|
+
performance.getEntriesByName = function(name, type) {
|
|
177
|
+
return __perfEntries.filter(function(e) { return e.name === name && (!type || e.entryType === type); });
|
|
178
|
+
};
|
|
179
|
+
performance.clearMarks = function(name) {
|
|
180
|
+
__perfEntries = __perfEntries.filter(function(e) {
|
|
181
|
+
return e.entryType !== 'mark' || (name !== undefined && e.name !== name);
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
performance.clearMeasures = function(name) {
|
|
185
|
+
__perfEntries = __perfEntries.filter(function(e) {
|
|
186
|
+
return e.entryType !== 'measure' || (name !== undefined && e.name !== name);
|
|
187
|
+
});
|
|
188
|
+
};
|
|
110
189
|
})();
|
|
111
190
|
)JS";
|
|
112
191
|
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#include "TreeWalkerBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kTreeWalkerBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
var FILTER_ACCEPT = 1, FILTER_REJECT = 2, FILTER_SKIP = 3;
|
|
11
|
+
|
|
12
|
+
var NodeFilter = {
|
|
13
|
+
FILTER_ACCEPT: FILTER_ACCEPT,
|
|
14
|
+
FILTER_REJECT: FILTER_REJECT,
|
|
15
|
+
FILTER_SKIP: FILTER_SKIP,
|
|
16
|
+
SHOW_ALL: 0xFFFFFFFF,
|
|
17
|
+
SHOW_ELEMENT: 0x1,
|
|
18
|
+
SHOW_ATTRIBUTE: 0x2,
|
|
19
|
+
SHOW_TEXT: 0x4,
|
|
20
|
+
SHOW_CDATA_SECTION: 0x8,
|
|
21
|
+
SHOW_ENTITY_REFERENCE: 0x10,
|
|
22
|
+
SHOW_ENTITY: 0x20,
|
|
23
|
+
SHOW_PROCESSING_INSTRUCTION: 0x40,
|
|
24
|
+
SHOW_COMMENT: 0x80,
|
|
25
|
+
SHOW_DOCUMENT: 0x100,
|
|
26
|
+
SHOW_DOCUMENT_TYPE: 0x200,
|
|
27
|
+
SHOW_DOCUMENT_FRAGMENT: 0x400,
|
|
28
|
+
SHOW_NOTATION: 0x800,
|
|
29
|
+
};
|
|
30
|
+
globalThis.NodeFilter = NodeFilter;
|
|
31
|
+
|
|
32
|
+
function normalizeFilter(filter) {
|
|
33
|
+
if (filter === undefined || filter === null) return null;
|
|
34
|
+
if (typeof filter === 'function') return filter;
|
|
35
|
+
if (typeof filter.acceptNode === 'function') {
|
|
36
|
+
return function(node) { return filter.acceptNode(node); };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Shared by TreeWalker and NodeIterator — both store whatToShow/_filterFn/
|
|
42
|
+
// _active the same way, so this reads `this` structurally rather than
|
|
43
|
+
// needing a common base class.
|
|
44
|
+
function applyFilter(walker, node) {
|
|
45
|
+
if (walker._active) {
|
|
46
|
+
throw new DOMException('Recursive node filtering', 'InvalidStateError');
|
|
47
|
+
}
|
|
48
|
+
var n = node.nodeType - 1;
|
|
49
|
+
if (!((1 << n) & walker.whatToShow)) return FILTER_SKIP;
|
|
50
|
+
if (walker._filterFn === null) return FILTER_ACCEPT;
|
|
51
|
+
walker._active = true;
|
|
52
|
+
var result;
|
|
53
|
+
try {
|
|
54
|
+
result = walker._filterFn(node);
|
|
55
|
+
} finally {
|
|
56
|
+
walker._active = false;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── TreeWalker ───────────────────────────────────────────────────────
|
|
62
|
+
// Ported from jsdom's TreeWalker-impl.js — see header comment.
|
|
63
|
+
|
|
64
|
+
function TreeWalker(root, whatToShow, filter) {
|
|
65
|
+
this.root = root;
|
|
66
|
+
this.whatToShow = whatToShow === undefined ? NodeFilter.SHOW_ALL : (whatToShow >>> 0);
|
|
67
|
+
this.filter = filter === undefined ? null : filter;
|
|
68
|
+
this._filterFn = normalizeFilter(filter);
|
|
69
|
+
this._active = false;
|
|
70
|
+
this._currentNode = root;
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(TreeWalker.prototype, 'currentNode', {
|
|
73
|
+
get: function() { return this._currentNode; },
|
|
74
|
+
set: function(node) {
|
|
75
|
+
if (node === null || node === undefined) {
|
|
76
|
+
throw new DOMException('Cannot set currentNode to null', 'NotSupportedError');
|
|
77
|
+
}
|
|
78
|
+
this._currentNode = node;
|
|
79
|
+
},
|
|
80
|
+
enumerable: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
TreeWalker.prototype.parentNode = function() {
|
|
84
|
+
var node = this._currentNode;
|
|
85
|
+
while (node !== null && node !== this.root) {
|
|
86
|
+
node = node.parentNode;
|
|
87
|
+
if (node !== null && applyFilter(this, node) === FILTER_ACCEPT) {
|
|
88
|
+
this._currentNode = node;
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function traverseChildren(walker, forward) {
|
|
96
|
+
var node = walker._currentNode;
|
|
97
|
+
node = forward ? node.firstChild : node.lastChild;
|
|
98
|
+
if (node === null) return null;
|
|
99
|
+
|
|
100
|
+
mainLoop:
|
|
101
|
+
for (;;) {
|
|
102
|
+
var result = applyFilter(walker, node);
|
|
103
|
+
if (result === FILTER_ACCEPT) {
|
|
104
|
+
walker._currentNode = node;
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
if (result === FILTER_SKIP) {
|
|
108
|
+
var child = forward ? node.firstChild : node.lastChild;
|
|
109
|
+
if (child !== null) { node = child; continue mainLoop; }
|
|
110
|
+
}
|
|
111
|
+
for (;;) {
|
|
112
|
+
var sibling = forward ? node.nextSibling : node.previousSibling;
|
|
113
|
+
if (sibling !== null) { node = sibling; continue mainLoop; }
|
|
114
|
+
var parent = node.parentNode;
|
|
115
|
+
if (parent === null || parent === walker.root || parent === walker._currentNode) return null;
|
|
116
|
+
node = parent;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
TreeWalker.prototype.firstChild = function() { return traverseChildren(this, true); };
|
|
121
|
+
TreeWalker.prototype.lastChild = function() { return traverseChildren(this, false); };
|
|
122
|
+
|
|
123
|
+
function traverseSiblings(walker, forward) {
|
|
124
|
+
var node = walker._currentNode;
|
|
125
|
+
if (node === walker.root) return null;
|
|
126
|
+
|
|
127
|
+
for (;;) {
|
|
128
|
+
var sibling = forward ? node.nextSibling : node.previousSibling;
|
|
129
|
+
while (sibling !== null) {
|
|
130
|
+
node = sibling;
|
|
131
|
+
var result = applyFilter(walker, node);
|
|
132
|
+
if (result === FILTER_ACCEPT) {
|
|
133
|
+
walker._currentNode = node;
|
|
134
|
+
return node;
|
|
135
|
+
}
|
|
136
|
+
sibling = forward ? node.firstChild : node.lastChild;
|
|
137
|
+
if (result === FILTER_REJECT || sibling === null) {
|
|
138
|
+
sibling = forward ? node.nextSibling : node.previousSibling;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
node = node.parentNode;
|
|
142
|
+
if (node === null || node === walker.root) return null;
|
|
143
|
+
if (applyFilter(walker, node) === FILTER_ACCEPT) return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
TreeWalker.prototype.previousSibling = function() { return traverseSiblings(this, false); };
|
|
147
|
+
TreeWalker.prototype.nextSibling = function() { return traverseSiblings(this, true); };
|
|
148
|
+
|
|
149
|
+
TreeWalker.prototype.previousNode = function() {
|
|
150
|
+
var node = this._currentNode;
|
|
151
|
+
while (node !== this.root) {
|
|
152
|
+
var sibling = node.previousSibling;
|
|
153
|
+
while (sibling !== null) {
|
|
154
|
+
node = sibling;
|
|
155
|
+
var result = applyFilter(this, node);
|
|
156
|
+
while (result !== FILTER_REJECT && node.firstChild !== null) {
|
|
157
|
+
node = node.lastChild;
|
|
158
|
+
result = applyFilter(this, node);
|
|
159
|
+
}
|
|
160
|
+
if (result === FILTER_ACCEPT) {
|
|
161
|
+
this._currentNode = node;
|
|
162
|
+
return node;
|
|
163
|
+
}
|
|
164
|
+
sibling = node.previousSibling;
|
|
165
|
+
}
|
|
166
|
+
if (node === this.root || node.parentNode === null) return null;
|
|
167
|
+
node = node.parentNode;
|
|
168
|
+
if (applyFilter(this, node) === FILTER_ACCEPT) {
|
|
169
|
+
this._currentNode = node;
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
TreeWalker.prototype.nextNode = function() {
|
|
177
|
+
var node = this._currentNode;
|
|
178
|
+
var result = FILTER_ACCEPT;
|
|
179
|
+
for (;;) {
|
|
180
|
+
while (result !== FILTER_REJECT && node.firstChild !== null) {
|
|
181
|
+
node = node.firstChild;
|
|
182
|
+
result = applyFilter(this, node);
|
|
183
|
+
if (result === FILTER_ACCEPT) {
|
|
184
|
+
this._currentNode = node;
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
var sibling = null;
|
|
189
|
+
do {
|
|
190
|
+
if (node === this.root) return null;
|
|
191
|
+
sibling = node.nextSibling;
|
|
192
|
+
if (sibling !== null) { node = sibling; break; }
|
|
193
|
+
node = node.parentNode;
|
|
194
|
+
} while (node !== null);
|
|
195
|
+
if (node === null) return null;
|
|
196
|
+
result = applyFilter(this, node);
|
|
197
|
+
if (result === FILTER_ACCEPT) {
|
|
198
|
+
this._currentNode = node;
|
|
199
|
+
return node;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
globalThis.TreeWalker = TreeWalker;
|
|
205
|
+
|
|
206
|
+
// ── NodeIterator ─────────────────────────────────────────────────────
|
|
207
|
+
// Ported from jsdom's NodeIterator-impl.js, with domSymbolTree.following()/
|
|
208
|
+
// preceding() replaced by direct tree-order walks over the same node
|
|
209
|
+
// properties TreeWalker uses.
|
|
210
|
+
|
|
211
|
+
function followingNode(node, root) {
|
|
212
|
+
if (node.firstChild !== null) return node.firstChild;
|
|
213
|
+
var cur = node;
|
|
214
|
+
while (cur !== null && cur !== root) {
|
|
215
|
+
if (cur.nextSibling !== null) return cur.nextSibling;
|
|
216
|
+
cur = cur.parentNode;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function lastInclusiveDescendant(node) {
|
|
221
|
+
while (node.lastChild !== null) node = node.lastChild;
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
function precedingNode(node, root) {
|
|
225
|
+
if (node === root) return null;
|
|
226
|
+
var sibling = node.previousSibling;
|
|
227
|
+
if (sibling !== null) return lastInclusiveDescendant(sibling);
|
|
228
|
+
return node.parentNode;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function NodeIterator(root, whatToShow, filter) {
|
|
232
|
+
this.root = root;
|
|
233
|
+
this.whatToShow = whatToShow === undefined ? NodeFilter.SHOW_ALL : (whatToShow >>> 0);
|
|
234
|
+
this.filter = filter === undefined ? null : filter;
|
|
235
|
+
this._filterFn = normalizeFilter(filter);
|
|
236
|
+
this._active = false;
|
|
237
|
+
this._referenceNode = root;
|
|
238
|
+
this._pointerBeforeReferenceNode = true;
|
|
239
|
+
}
|
|
240
|
+
Object.defineProperty(NodeIterator.prototype, 'referenceNode', {
|
|
241
|
+
get: function() { return this._referenceNode; },
|
|
242
|
+
enumerable: true,
|
|
243
|
+
});
|
|
244
|
+
Object.defineProperty(NodeIterator.prototype, 'pointerBeforeReferenceNode', {
|
|
245
|
+
get: function() { return this._pointerBeforeReferenceNode; },
|
|
246
|
+
enumerable: true,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
function iteratorTraverse(it, forward) {
|
|
250
|
+
var node = it._referenceNode;
|
|
251
|
+
var beforeNode = it._pointerBeforeReferenceNode;
|
|
252
|
+
for (;;) {
|
|
253
|
+
if (forward) {
|
|
254
|
+
if (!beforeNode) {
|
|
255
|
+
node = followingNode(node, it.root);
|
|
256
|
+
if (node === null) return null;
|
|
257
|
+
}
|
|
258
|
+
beforeNode = false;
|
|
259
|
+
} else {
|
|
260
|
+
if (beforeNode) {
|
|
261
|
+
node = precedingNode(node, it.root);
|
|
262
|
+
if (node === null) return null;
|
|
263
|
+
}
|
|
264
|
+
beforeNode = true;
|
|
265
|
+
}
|
|
266
|
+
if (applyFilter(it, node) === FILTER_ACCEPT) break;
|
|
267
|
+
}
|
|
268
|
+
it._referenceNode = node;
|
|
269
|
+
it._pointerBeforeReferenceNode = beforeNode;
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
NodeIterator.prototype.nextNode = function() { return iteratorTraverse(this, true); };
|
|
274
|
+
NodeIterator.prototype.previousNode = function() { return iteratorTraverse(this, false); };
|
|
275
|
+
NodeIterator.prototype.detach = function() {};
|
|
276
|
+
|
|
277
|
+
globalThis.NodeIterator = NodeIterator;
|
|
278
|
+
|
|
279
|
+
// ── document.createTreeWalker / createNodeIterator ──────────────────
|
|
280
|
+
document.createTreeWalker = function(root, whatToShow, filter) {
|
|
281
|
+
if (!(root instanceof Node)) {
|
|
282
|
+
throw new TypeError("Failed to execute 'createTreeWalker' on 'Document': parameter 1 is not of type 'Node'.");
|
|
283
|
+
}
|
|
284
|
+
return new TreeWalker(root, whatToShow, filter);
|
|
285
|
+
};
|
|
286
|
+
document.createNodeIterator = function(root, whatToShow, filter) {
|
|
287
|
+
if (!(root instanceof Node)) {
|
|
288
|
+
throw new TypeError("Failed to execute 'createNodeIterator' on 'Document': parameter 1 is not of type 'Node'.");
|
|
289
|
+
}
|
|
290
|
+
return new NodeIterator(root, whatToShow, filter);
|
|
291
|
+
};
|
|
292
|
+
})();
|
|
293
|
+
)JS";
|
|
294
|
+
|
|
295
|
+
} // namespace
|
|
296
|
+
|
|
297
|
+
void TreeWalkerBindings::install(JSContext* ctx) {
|
|
298
|
+
JSValue result = JS_Eval(ctx, kTreeWalkerBootstrapScript, strlen(kTreeWalkerBootstrapScript),
|
|
299
|
+
"<tree-walker-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
300
|
+
if (JS_IsException(result)) {
|
|
301
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
302
|
+
}
|
|
303
|
+
JS_FreeValue(ctx, result);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// NodeFilter, TreeWalker, NodeIterator, and document.createTreeWalker() /
|
|
4
|
+
// createNodeIterator(). Pure JS on top of the Node traversal properties
|
|
5
|
+
// ElementBindings already exposes (firstChild/lastChild/nextSibling/
|
|
6
|
+
// previousSibling/parentNode/nodeType) — no Lexbor access needed.
|
|
7
|
+
//
|
|
8
|
+
// The traversal algorithms (TreeWalker.previousSibling()/nextSibling() in
|
|
9
|
+
// particular — they can descend into an accepted-skip subtree looking for a
|
|
10
|
+
// matching descendant, then ascend) are subtle enough that this is a direct
|
|
11
|
+
// port of jsdom's TreeWalker-impl.js/NodeIterator-impl.js/helpers.js, not a
|
|
12
|
+
// from-scratch implementation, to avoid a plausible-looking-but-wrong
|
|
13
|
+
// backtracking bug. NodeIterator's `_referenceNode`/`_pointerBeforeReferenceNode`
|
|
14
|
+
// walk uses simple tree-order successor/predecessor helpers here in place of
|
|
15
|
+
// jsdom's domSymbolTree.following()/preceding() dependency.
|
|
16
|
+
//
|
|
17
|
+
// Must run after ElementBindings (globalThis.Element/Node) and
|
|
18
|
+
// DocumentBindings (globalThis.document).
|
|
19
|
+
|
|
20
|
+
#include "quickjs.h"
|
|
21
|
+
|
|
22
|
+
namespace margelo::nitro::nitrojsdom {
|
|
23
|
+
|
|
24
|
+
struct TreeWalkerBindings {
|
|
25
|
+
static void install(JSContext* ctx);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -268,7 +268,50 @@ const char* kUrlBootstrapScript = R"JS(
|
|
|
268
268
|
URL.prototype.toString = function() { return this._href; };
|
|
269
269
|
URL.prototype.toJSON = function() { return this._href; };
|
|
270
270
|
|
|
271
|
+
URL.canParse = function(url, base) {
|
|
272
|
+
try {
|
|
273
|
+
new URL(url, base);
|
|
274
|
+
return true;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
271
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
|
+
});
|
|
272
315
|
})();
|
|
273
316
|
)JS";
|
|
274
317
|
|