@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.
- package/android/CMakeLists.txt +18 -0
- package/cpp/HybridHtmlSandbox.cpp +20 -3
- package/cpp/HybridHtmlSandbox.hpp +1 -1
- package/cpp/lexbor/LexborDocument.cpp +279 -67
- package/cpp/lexbor/LexborDocument.hpp +43 -10
- package/cpp/quickjs/DOMBindings.cpp +37 -1
- package/cpp/quickjs/DOMBindingsInternal.cpp +101 -2
- package/cpp/quickjs/DOMBindingsInternal.hpp +56 -1
- package/cpp/quickjs/QuickJSRuntime.cpp +32 -4
- package/cpp/quickjs/QuickJSRuntime.hpp +50 -1
- package/cpp/quickjs/bindings/AbortBindings.cpp +128 -0
- package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
- package/cpp/quickjs/bindings/BlobBindings.cpp +164 -0
- package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
- package/cpp/quickjs/bindings/CSSOMBindings.cpp +290 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +25 -0
- package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
- package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
- package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
- package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
- package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +239 -4
- package/cpp/quickjs/bindings/ElementBindings.cpp +556 -44
- package/cpp/quickjs/bindings/EventBindings.cpp +414 -8
- 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 +45 -6
- package/cpp/quickjs/bindings/FormBindings.cpp +365 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +20 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
- package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +228 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
- package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
- package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
- package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
- package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +120 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
- package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
- package/cpp/quickjs/bindings/UrlBindings.cpp +295 -0
- package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +552 -0
- package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
- package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
- package/package.json +17 -2
- package/src/classes/JSDOM/JSDOM.class.ts +1 -0
- 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 `content` getter on Element.prototype, tag-checked for
|
|
8
|
+
// "template" internally (mirroring FormBindings' tag-check pattern) —
|
|
9
|
+
// returns the lxb_dom_document_fragment_t Lexbor already allocates as
|
|
10
|
+
// lxb_html_template_element_t::content, for both parsed and
|
|
11
|
+
// document.createElement()'d <template> elements. No custom parsing logic:
|
|
12
|
+
// Lexbor's own HTML5 tree constructor already redirects a parsed
|
|
13
|
+
// <template>'s children into that fragment instead of the template's own
|
|
14
|
+
// light-DOM children (see packages/lexbor/.../tree/insertion_mode/in_template.c).
|
|
15
|
+
// LexborDocument::setInnerHTMLOnEl() mirrors that for template.innerHTML=.
|
|
16
|
+
//
|
|
17
|
+
// Scope: the returned fragment is a generic Node wrapper (like
|
|
18
|
+
// document.createDocumentFragment()), so it has childNodes/appendChild/etc.
|
|
19
|
+
// but not querySelector(All) — same limitation createDocumentFragment()
|
|
20
|
+
// already has. cloneNode() on a <template> does not specially deep-clone
|
|
21
|
+
// `.content` (relies on Lexbor's own node clone, not verified against spec
|
|
22
|
+
// for this case) — undocumented/untested edge case, not the golden path.
|
|
23
|
+
//
|
|
24
|
+
// Must run after ElementBindings (globalThis.Element, unwrap_element).
|
|
25
|
+
struct TemplateBindings {
|
|
26
|
+
static void install(JSContext* ctx);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#include "TextEncodingBindings.hpp"
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
namespace margelo::nitro::nitrojsdom {
|
|
5
|
+
|
|
6
|
+
namespace {
|
|
7
|
+
|
|
8
|
+
const char* kTextEncodingBootstrapScript = R"JS(
|
|
9
|
+
(function() {
|
|
10
|
+
function TextEncoder() {
|
|
11
|
+
this.encoding = 'utf-8';
|
|
12
|
+
}
|
|
13
|
+
TextEncoder.prototype.encode = function(input) {
|
|
14
|
+
var str = String(input === undefined ? '' : input);
|
|
15
|
+
var bytes = [];
|
|
16
|
+
for (var i = 0; i < str.length; i++) {
|
|
17
|
+
var code = str.charCodeAt(i);
|
|
18
|
+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
|
|
19
|
+
var next = str.charCodeAt(i + 1);
|
|
20
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
21
|
+
code = ((code - 0xD800) << 10) + (next - 0xDC00) + 0x10000;
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (code < 0x80) {
|
|
26
|
+
bytes.push(code);
|
|
27
|
+
} else if (code < 0x800) {
|
|
28
|
+
bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
|
|
29
|
+
} else if (code < 0x10000) {
|
|
30
|
+
bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
|
|
31
|
+
} else {
|
|
32
|
+
bytes.push(
|
|
33
|
+
0xF0 | (code >> 18),
|
|
34
|
+
0x80 | ((code >> 12) & 0x3F),
|
|
35
|
+
0x80 | ((code >> 6) & 0x3F),
|
|
36
|
+
0x80 | (code & 0x3F)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return new Uint8Array(bytes);
|
|
41
|
+
};
|
|
42
|
+
TextEncoder.prototype.encodeInto = function(input, dest) {
|
|
43
|
+
var encoded = this.encode(input);
|
|
44
|
+
var written = Math.min(encoded.length, dest.length);
|
|
45
|
+
for (var i = 0; i < written; i++) dest[i] = encoded[i];
|
|
46
|
+
return { read: written === encoded.length ? String(input).length : written, written: written };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function TextDecoder(label, options) {
|
|
50
|
+
this.encoding = label ? String(label).toLowerCase() : 'utf-8';
|
|
51
|
+
this.fatal = !!(options && options.fatal);
|
|
52
|
+
this.ignoreBOM = !!(options && options.ignoreBOM);
|
|
53
|
+
}
|
|
54
|
+
TextDecoder.prototype.decode = function(input) {
|
|
55
|
+
var bytes;
|
|
56
|
+
if (input === undefined) {
|
|
57
|
+
bytes = new Uint8Array(0);
|
|
58
|
+
} else if (input instanceof ArrayBuffer) {
|
|
59
|
+
bytes = new Uint8Array(input);
|
|
60
|
+
} else if (input && input.buffer !== undefined) {
|
|
61
|
+
bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
62
|
+
} else {
|
|
63
|
+
bytes = new Uint8Array(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var result = '';
|
|
67
|
+
var i = 0;
|
|
68
|
+
while (i < bytes.length) {
|
|
69
|
+
var b0 = bytes[i];
|
|
70
|
+
var codepoint, len;
|
|
71
|
+
if (b0 < 0x80) { codepoint = b0; len = 1; }
|
|
72
|
+
else if ((b0 & 0xE0) === 0xC0) { codepoint = b0 & 0x1F; len = 2; }
|
|
73
|
+
else if ((b0 & 0xF0) === 0xE0) { codepoint = b0 & 0x0F; len = 3; }
|
|
74
|
+
else if ((b0 & 0xF8) === 0xF0) { codepoint = b0 & 0x07; len = 4; }
|
|
75
|
+
else {
|
|
76
|
+
if (this.fatal) throw new TypeError('The encoded data was not valid UTF-8.');
|
|
77
|
+
result += '�';
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (i + len > bytes.length) {
|
|
82
|
+
if (this.fatal) throw new TypeError('The encoded data was not valid UTF-8.');
|
|
83
|
+
result += '�';
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
for (var k = 1; k < len; k++) {
|
|
87
|
+
codepoint = (codepoint << 6) | (bytes[i + k] & 0x3F);
|
|
88
|
+
}
|
|
89
|
+
if (codepoint > 0xFFFF) {
|
|
90
|
+
codepoint -= 0x10000;
|
|
91
|
+
result += String.fromCharCode(0xD800 + (codepoint >> 10), 0xDC00 + (codepoint & 0x3FF));
|
|
92
|
+
} else {
|
|
93
|
+
result += String.fromCharCode(codepoint);
|
|
94
|
+
}
|
|
95
|
+
i += len;
|
|
96
|
+
}
|
|
97
|
+
if (!this.ignoreBOM && result.charCodeAt(0) === 0xFEFF) result = result.slice(1);
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
globalThis.TextEncoder = TextEncoder;
|
|
102
|
+
globalThis.TextDecoder = TextDecoder;
|
|
103
|
+
})();
|
|
104
|
+
)JS";
|
|
105
|
+
|
|
106
|
+
} // namespace
|
|
107
|
+
|
|
108
|
+
void TextEncodingBindings::install(JSContext* ctx) {
|
|
109
|
+
JSValue result = JS_Eval(ctx, kTextEncodingBootstrapScript, strlen(kTextEncodingBootstrapScript),
|
|
110
|
+
"<text-encoding-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
111
|
+
if (JS_IsException(result)) {
|
|
112
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
113
|
+
}
|
|
114
|
+
JS_FreeValue(ctx, result);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
} // 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.TextEncoder/TextDecoder (UTF-8 only — the vast
|
|
8
|
+
// majority of real-world usage — implemented in pure JS since encoding a
|
|
9
|
+
// JS string's UTF-16 code units to UTF-8 bytes needs no native primitive).
|
|
10
|
+
struct TextEncodingBindings {
|
|
11
|
+
static void install(JSContext* ctx);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "../DOMBindingsInternal.hpp"
|
|
3
3
|
#include "../QuickJSRuntime.hpp"
|
|
4
4
|
#include <chrono>
|
|
5
|
+
#include <cstring>
|
|
5
6
|
|
|
6
7
|
namespace margelo::nitro::nitrojsdom {
|
|
7
8
|
|
|
@@ -12,6 +13,17 @@ int64_t dom_now_ms() {
|
|
|
12
13
|
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
double dom_now_high_res_ms() {
|
|
17
|
+
using namespace std::chrono;
|
|
18
|
+
return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
JSValue js_native_now_ms(JSContext* ctx, JSValue, int, JSValue*) {
|
|
22
|
+
auto* rctx = get_ctx(ctx);
|
|
23
|
+
double origin = rctx ? rctx->time_origin_ms : 0.0;
|
|
24
|
+
return JS_NewFloat64(ctx, dom_now_high_res_ms() - origin);
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
JSValue js_setTimeout(JSContext* ctx, JSValue, int argc, JSValue* argv) {
|
|
16
28
|
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_NewInt32(ctx, 0);
|
|
17
29
|
auto* rctx = get_ctx(ctx);
|
|
@@ -80,15 +92,123 @@ JSValue js_clearTimer(JSContext* ctx, JSValue, int argc, JSValue* argv) {
|
|
|
80
92
|
return JS_UNDEFINED;
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
const char* kTimerBootstrapScript = R"JS(
|
|
96
|
+
(function() {
|
|
97
|
+
globalThis.performance = globalThis.performance || {};
|
|
98
|
+
globalThis.performance.now = function() { return __nativeNowMs(); };
|
|
99
|
+
|
|
100
|
+
globalThis.requestAnimationFrame = function(callback) {
|
|
101
|
+
return setTimeout(function() { callback(performance.now()); }, 16);
|
|
102
|
+
};
|
|
103
|
+
globalThis.cancelAnimationFrame = function(id) {
|
|
104
|
+
clearTimeout(id);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
globalThis.queueMicrotask = function(callback) {
|
|
108
|
+
Promise.resolve().then(function() { callback(); });
|
|
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
|
+
};
|
|
189
|
+
})();
|
|
190
|
+
)JS";
|
|
191
|
+
|
|
83
192
|
} // namespace
|
|
84
193
|
|
|
85
194
|
void TimerBindings::install(JSContext* ctx) {
|
|
195
|
+
auto* rctx = get_ctx(ctx);
|
|
196
|
+
if (rctx) rctx->time_origin_ms = dom_now_high_res_ms();
|
|
197
|
+
|
|
86
198
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
87
199
|
JS_SetPropertyStr(ctx, global, "setTimeout", JS_NewCFunction(ctx, js_setTimeout, "setTimeout", 2));
|
|
88
200
|
JS_SetPropertyStr(ctx, global, "setInterval", JS_NewCFunction(ctx, js_setInterval, "setInterval", 2));
|
|
89
201
|
JS_SetPropertyStr(ctx, global, "clearTimeout", JS_NewCFunction(ctx, js_clearTimer, "clearTimeout", 1));
|
|
90
202
|
JS_SetPropertyStr(ctx, global, "clearInterval", JS_NewCFunction(ctx, js_clearTimer, "clearInterval",1));
|
|
203
|
+
JS_SetPropertyStr(ctx, global, "__nativeNowMs", JS_NewCFunction(ctx, js_native_now_ms, "__nativeNowMs", 0));
|
|
91
204
|
JS_FreeValue(ctx, global);
|
|
205
|
+
|
|
206
|
+
JSValue result = JS_Eval(ctx, kTimerBootstrapScript, strlen(kTimerBootstrapScript),
|
|
207
|
+
"<timer-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
208
|
+
if (JS_IsException(result)) {
|
|
209
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
210
|
+
}
|
|
211
|
+
JS_FreeValue(ctx, result);
|
|
92
212
|
}
|
|
93
213
|
|
|
94
214
|
} // namespace margelo::nitro::nitrojsdom
|
|
@@ -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
|