@salve-software/react-native-nitro-jsdom 1.1.0 → 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 +4 -0
- package/cpp/HybridHtmlSandbox.cpp +7 -3
- package/cpp/lexbor/LexborDocument.cpp +62 -0
- package/cpp/lexbor/LexborDocument.hpp +14 -0
- package/cpp/quickjs/DOMBindings.cpp +9 -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 +28 -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 +46 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
- package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
- package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +166 -11
- package/cpp/quickjs/bindings/ElementBindings.cpp +296 -20
- package/cpp/quickjs/bindings/EventBindings.cpp +277 -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 +223 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
- 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 +9 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +286 -4
- package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
- package/package.json +17 -2
package/android/CMakeLists.txt
CHANGED
|
@@ -52,6 +52,7 @@ add_library(${PACKAGE_NAME} SHARED
|
|
|
52
52
|
../cpp/quickjs/bindings/StyleBindings.cpp
|
|
53
53
|
../cpp/quickjs/bindings/ElementBindings.cpp
|
|
54
54
|
../cpp/quickjs/bindings/DocumentBindings.cpp
|
|
55
|
+
../cpp/quickjs/bindings/DOMParserBindings.cpp
|
|
55
56
|
../cpp/quickjs/bindings/EventBindings.cpp
|
|
56
57
|
../cpp/quickjs/bindings/TimerBindings.cpp
|
|
57
58
|
../cpp/quickjs/bindings/WindowBindings.cpp
|
|
@@ -69,6 +70,9 @@ add_library(${PACKAGE_NAME} SHARED
|
|
|
69
70
|
../cpp/quickjs/bindings/TemplateBindings.cpp
|
|
70
71
|
../cpp/quickjs/bindings/SlotBindings.cpp
|
|
71
72
|
../cpp/quickjs/bindings/XmlSerializerBindings.cpp
|
|
73
|
+
../cpp/quickjs/bindings/LayoutStubBindings.cpp
|
|
74
|
+
../cpp/quickjs/bindings/TreeWalkerBindings.cpp
|
|
75
|
+
../cpp/quickjs/bindings/EventTargetBindings.cpp
|
|
72
76
|
${QUICKJS_SOURCES}
|
|
73
77
|
)
|
|
74
78
|
|
|
@@ -55,10 +55,14 @@ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, con
|
|
|
55
55
|
// or window.addEventListener(...) — the common embedded-widget bootstrap
|
|
56
56
|
// pattern — see them. There's no real subresource loading in this sandbox,
|
|
57
57
|
// so both fire back-to-back with nothing in between.
|
|
58
|
+
_runtime->contextState()->ready_state = "interactive";
|
|
58
59
|
try {
|
|
59
|
-
_runtime->evaluate(
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
_runtime->evaluate("document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true }));");
|
|
61
|
+
} catch (...) { }
|
|
62
|
+
|
|
63
|
+
_runtime->contextState()->ready_state = "complete";
|
|
64
|
+
try {
|
|
65
|
+
_runtime->evaluate("document.dispatchEvent(new Event('load'));");
|
|
62
66
|
} catch (...) { }
|
|
63
67
|
|
|
64
68
|
_initialized = true;
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
#include <lexbor/dom/dom.h>
|
|
7
7
|
#include <lexbor/dom/interfaces/character_data.h>
|
|
8
8
|
#include <lexbor/dom/interfaces/document_type.h>
|
|
9
|
+
#include <lexbor/dom/interfaces/element.h>
|
|
10
|
+
#include <lexbor/ns/ns.h>
|
|
11
|
+
#include <lexbor/tag/tag.h>
|
|
9
12
|
#include <stdexcept>
|
|
10
13
|
#include <cctype>
|
|
11
14
|
#include <cstring>
|
|
@@ -230,6 +233,65 @@ void* LexborDocument::createElement(const std::string& tag) {
|
|
|
230
233
|
reinterpret_cast<const lxb_char_t*>(tag.data()), tag.size(), nullptr);
|
|
231
234
|
}
|
|
232
235
|
|
|
236
|
+
void* LexborDocument::createElementNS(const std::string& nsUri, const std::string& qualifiedName) {
|
|
237
|
+
if (!_document) return nullptr;
|
|
238
|
+
auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
|
|
239
|
+
|
|
240
|
+
std::string prefix;
|
|
241
|
+
std::string localName = qualifiedName;
|
|
242
|
+
auto colon = qualifiedName.find(':');
|
|
243
|
+
if (colon != std::string::npos) {
|
|
244
|
+
prefix = qualifiedName.substr(0, colon);
|
|
245
|
+
localName = qualifiedName.substr(colon + 1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
bool known_tag = lxb_tag_data_by_name(dom_doc->tags,
|
|
249
|
+
reinterpret_cast<const lxb_char_t*>(localName.data()), localName.size()) != nullptr;
|
|
250
|
+
|
|
251
|
+
if (!known_tag) {
|
|
252
|
+
return lxb_dom_element_create(dom_doc,
|
|
253
|
+
reinterpret_cast<const lxb_char_t*>(localName.data()), localName.size(),
|
|
254
|
+
nsUri.empty() ? nullptr : reinterpret_cast<const lxb_char_t*>(nsUri.data()), nsUri.size(),
|
|
255
|
+
prefix.empty() ? nullptr : reinterpret_cast<const lxb_char_t*>(prefix.data()), prefix.size(),
|
|
256
|
+
nullptr, 0, true);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Known tag + arbitrary ns crashes Lexbor's HTML interface dispatch, so
|
|
260
|
+
// resolve the ns id via a throwaway unknown-tag element first (safe path).
|
|
261
|
+
static const char* kNsProbeTag = "x-nitrojsdom-ns-probe";
|
|
262
|
+
lxb_dom_element_t* probe = lxb_dom_element_create(dom_doc,
|
|
263
|
+
reinterpret_cast<const lxb_char_t*>(kNsProbeTag), strlen(kNsProbeTag),
|
|
264
|
+
nsUri.empty() ? nullptr : reinterpret_cast<const lxb_char_t*>(nsUri.data()), nsUri.size(),
|
|
265
|
+
nullptr, 0, nullptr, 0, true);
|
|
266
|
+
if (!probe) return nullptr;
|
|
267
|
+
lxb_ns_id_t resolved_ns = probe->node.ns;
|
|
268
|
+
lxb_dom_element_destroy(probe);
|
|
269
|
+
|
|
270
|
+
lxb_dom_element_t* element = lxb_dom_document_create_element(dom_doc,
|
|
271
|
+
reinterpret_cast<const lxb_char_t*>(localName.data()), localName.size(), nullptr);
|
|
272
|
+
if (!element) return nullptr;
|
|
273
|
+
element->node.ns = resolved_ns;
|
|
274
|
+
|
|
275
|
+
if (!prefix.empty()) {
|
|
276
|
+
const lxb_ns_prefix_data_t* ns_prefix = lxb_ns_prefix_append(dom_doc->prefix,
|
|
277
|
+
reinterpret_cast<const lxb_char_t*>(prefix.data()), prefix.size());
|
|
278
|
+
if (ns_prefix) element->node.prefix = ns_prefix->prefix_id;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return element;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
std::string LexborDocument::namespaceURI(void* node) const {
|
|
285
|
+
if (!_document || !node) return "";
|
|
286
|
+
auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
|
|
287
|
+
auto* n = static_cast<lxb_dom_node_t*>(node);
|
|
288
|
+
if (n->ns == LXB_NS__UNDEF) return "";
|
|
289
|
+
size_t len = 0;
|
|
290
|
+
const lxb_char_t* uri = lxb_ns_by_id(dom_doc->ns, static_cast<lxb_ns_id_t>(n->ns), &len);
|
|
291
|
+
if (!uri || len == 0) return "";
|
|
292
|
+
return std::string(reinterpret_cast<const char*>(uri), len);
|
|
293
|
+
}
|
|
294
|
+
|
|
233
295
|
void* LexborDocument::createTextNode(const std::string& text) {
|
|
234
296
|
if (!_document) return nullptr;
|
|
235
297
|
auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
|
|
@@ -38,6 +38,20 @@ public:
|
|
|
38
38
|
void* createComment(const std::string& text);
|
|
39
39
|
void* createDocumentFragment();
|
|
40
40
|
|
|
41
|
+
// ── Namespaces (createElementNS / Element.namespaceURI) ───────────────────
|
|
42
|
+
// qualifiedName may be "prefix:localName" or a bare "localName". nsUri may be
|
|
43
|
+
// empty for the null namespace. Lexbor registers arbitrary namespace URIs
|
|
44
|
+
// dynamically (lxb_ns_append), so any URI works, not just HTML/SVG/MathML.
|
|
45
|
+
// Note: lxb_tag_append_lower lowercases local_name internally, so
|
|
46
|
+
// mixed-case foreign tag/attribute names (e.g. SVG's "viewBox",
|
|
47
|
+
// "linearGradient") come back lowercased — a lexbor limitation, not
|
|
48
|
+
// something this binding can work around.
|
|
49
|
+
void* createElementNS(const std::string& nsUri, const std::string& qualifiedName);
|
|
50
|
+
|
|
51
|
+
// Returns the namespace URI for a node, or "" if the node has no namespace
|
|
52
|
+
// (LXB_NS__UNDEF) — callers should map "" to JS null.
|
|
53
|
+
std::string namespaceURI(void* node) const;
|
|
54
|
+
|
|
41
55
|
// ── Shadow DOM ─────────────────────────────────────────────────────────────
|
|
42
56
|
// mode: 0 = open, 1 = closed (matches lxb_dom_shadow_root_mode_t).
|
|
43
57
|
void* createShadowRoot(void* hostElement, int mode);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "bindings/DOMExceptionBindings.hpp"
|
|
8
8
|
#include "bindings/ElementBindings.hpp"
|
|
9
9
|
#include "bindings/DocumentBindings.hpp"
|
|
10
|
+
#include "bindings/DOMParserBindings.hpp"
|
|
10
11
|
#include "bindings/EventBindings.hpp"
|
|
11
12
|
#include "bindings/TimerBindings.hpp"
|
|
12
13
|
#include "bindings/WindowBindings.hpp"
|
|
@@ -24,6 +25,9 @@
|
|
|
24
25
|
#include "bindings/TemplateBindings.hpp"
|
|
25
26
|
#include "bindings/SlotBindings.hpp"
|
|
26
27
|
#include "bindings/XmlSerializerBindings.hpp"
|
|
28
|
+
#include "bindings/LayoutStubBindings.hpp"
|
|
29
|
+
#include "bindings/TreeWalkerBindings.hpp"
|
|
30
|
+
#include "bindings/EventTargetBindings.hpp"
|
|
27
31
|
#include <lexbor/html/html.h>
|
|
28
32
|
#include <lexbor/dom/dom.h>
|
|
29
33
|
|
|
@@ -36,13 +40,14 @@ namespace margelo::nitro::nitrojsdom {
|
|
|
36
40
|
|
|
37
41
|
void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
|
|
38
42
|
JSContext* ctx = static_cast<JSContext*>(runtime->context());
|
|
39
|
-
(
|
|
43
|
+
register_document(ctx, document); // so doc_for_node() resolves nodes back to the primary document
|
|
40
44
|
|
|
41
45
|
DOMExceptionBindings::install(ctx); // no dependencies; other modules throw DOMException instances
|
|
42
46
|
LiveCollectionBindings::install(ctx);
|
|
43
47
|
ElementBindings::install(ctx); // registers the Element class + proto
|
|
44
48
|
ShadowRootBindings::install(ctx); // needs Element's proto + js_node_class_id's proto
|
|
45
49
|
DocumentBindings::install(ctx); // creates globalThis.document
|
|
50
|
+
DOMParserBindings::install(ctx); // adds document.implementation + globalThis.DOMParser; needs globalThis.document
|
|
46
51
|
CookieBindings::install(ctx); // needs globalThis.document to exist
|
|
47
52
|
TemplateBindings::install(ctx); // needs Element's proto
|
|
48
53
|
CustomElementsBindings::install(ctx); // needs Element/ShadowRoot protos + globalThis.document
|
|
@@ -58,6 +63,9 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
|
|
|
58
63
|
FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
|
|
59
64
|
SlotBindings::install(ctx); // uses Event/dispatchEvent + Element/ShadowRoot protos, so must run after EventBindings/ShadowRootBindings
|
|
60
65
|
XmlSerializerBindings::install(ctx); // pure serialization, no ordering requirement beyond ElementBindings
|
|
66
|
+
LayoutStubBindings::install(ctx); // needs Element's proto + globalThis.document to exist
|
|
67
|
+
TreeWalkerBindings::install(ctx); // needs Element's proto (Node traversal props) + globalThis.document to exist
|
|
68
|
+
EventTargetBindings::install(ctx); // no dependencies
|
|
61
69
|
|
|
62
70
|
// ── localStorage / sessionStorage ──────────────────────────────────────────
|
|
63
71
|
{
|
|
@@ -126,6 +126,40 @@ LexborDocument* get_doc(JSContext* ctx) {
|
|
|
126
126
|
return rctx ? rctx->document : nullptr;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
LexborDocument* doc_for_node(JSContext* ctx, void* node) {
|
|
130
|
+
auto* rctx = get_ctx(ctx);
|
|
131
|
+
if (!rctx) return nullptr;
|
|
132
|
+
if (node) {
|
|
133
|
+
auto* dom_node = static_cast<lxb_dom_node_t*>(node);
|
|
134
|
+
auto it = rctx->document_registry.find(dom_node->owner_document);
|
|
135
|
+
if (it != rctx->document_registry.end()) return it->second;
|
|
136
|
+
}
|
|
137
|
+
return rctx->document;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void register_document(JSContext* ctx, LexborDocument* doc) {
|
|
141
|
+
auto* rctx = get_ctx(ctx);
|
|
142
|
+
if (!rctx || !doc) return;
|
|
143
|
+
auto* html_doc = static_cast<lxb_html_document_t*>(doc->documentHtmlPtr());
|
|
144
|
+
if (!html_doc) return;
|
|
145
|
+
void* dom_doc = lxb_dom_interface_document(html_doc);
|
|
146
|
+
rctx->document_registry[dom_doc] = doc;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void register_document_wrapper(JSContext* ctx, LexborDocument* doc, JSValue wrapper) {
|
|
150
|
+
auto* rctx = get_ctx(ctx);
|
|
151
|
+
if (!rctx || !doc) return;
|
|
152
|
+
rctx->document_wrappers[doc] = new JSValue(JS_DupValue(ctx, wrapper));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
JSValue get_document_wrapper(JSContext* ctx, LexborDocument* doc) {
|
|
156
|
+
auto* rctx = get_ctx(ctx);
|
|
157
|
+
if (!rctx || !doc) return JS_UNDEFINED;
|
|
158
|
+
auto it = rctx->document_wrappers.find(doc);
|
|
159
|
+
if (it == rctx->document_wrappers.end()) return JS_UNDEFINED;
|
|
160
|
+
return JS_DupValue(ctx, *static_cast<JSValue*>(it->second));
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter, SetterFn setter) {
|
|
130
164
|
JSAtom atom = JS_NewAtom(ctx, name);
|
|
131
165
|
JSValue get_fn = JS_NewCFunction2(ctx, (JSCFunction*)getter, name, 0, JS_CFUNC_getter, 0);
|
|
@@ -136,6 +170,39 @@ void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter,
|
|
|
136
170
|
JS_FreeAtom(ctx, atom);
|
|
137
171
|
}
|
|
138
172
|
|
|
173
|
+
void define_node_type_constants(JSContext* ctx, JSValue obj) {
|
|
174
|
+
static const struct { const char* name; int32_t value; } kNodeTypeConstants[] = {
|
|
175
|
+
{ "ELEMENT_NODE", 1 },
|
|
176
|
+
{ "ATTRIBUTE_NODE", 2 },
|
|
177
|
+
{ "TEXT_NODE", 3 },
|
|
178
|
+
{ "CDATA_SECTION_NODE", 4 },
|
|
179
|
+
{ "ENTITY_REFERENCE_NODE", 5 },
|
|
180
|
+
{ "ENTITY_NODE", 6 },
|
|
181
|
+
{ "PROCESSING_INSTRUCTION_NODE", 7 },
|
|
182
|
+
{ "COMMENT_NODE", 8 },
|
|
183
|
+
{ "DOCUMENT_NODE", 9 },
|
|
184
|
+
{ "DOCUMENT_TYPE_NODE", 10 },
|
|
185
|
+
{ "DOCUMENT_FRAGMENT_NODE", 11 },
|
|
186
|
+
{ "NOTATION_NODE", 12 },
|
|
187
|
+
};
|
|
188
|
+
for (const auto& c : kNodeTypeConstants) {
|
|
189
|
+
JS_DefinePropertyValueStr(ctx, obj, c.name, JS_NewInt32(ctx, c.value), JS_PROP_ENUMERABLE);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
JSValue build_doctype_object(JSContext* ctx, LexborDocument* doc, void* doctype) {
|
|
194
|
+
JSValue obj = JS_NewObject(ctx);
|
|
195
|
+
std::string name = doc->doctypeName(doctype);
|
|
196
|
+
JS_SetPropertyStr(ctx, obj, "name", JS_NewStringLen(ctx, name.data(), name.size()));
|
|
197
|
+
std::string publicId = doc->doctypePublicId(doctype);
|
|
198
|
+
JS_SetPropertyStr(ctx, obj, "publicId", JS_NewStringLen(ctx, publicId.data(), publicId.size()));
|
|
199
|
+
std::string systemId = doc->doctypeSystemId(doctype);
|
|
200
|
+
JS_SetPropertyStr(ctx, obj, "systemId", JS_NewStringLen(ctx, systemId.data(), systemId.size()));
|
|
201
|
+
JS_SetPropertyStr(ctx, obj, "nodeType", JS_NewInt32(ctx, 10));
|
|
202
|
+
JS_SetPropertyStr(ctx, obj, "nodeName", JS_NewStringLen(ctx, name.data(), name.size()));
|
|
203
|
+
return obj;
|
|
204
|
+
}
|
|
205
|
+
|
|
139
206
|
JSValue js_illegal_constructor(JSContext* ctx, JSValue, int, JSValue*) {
|
|
140
207
|
return JS_ThrowTypeError(ctx, "Illegal constructor");
|
|
141
208
|
}
|
|
@@ -56,6 +56,34 @@ std::string serialize_node(lxb_dom_node_t* node);
|
|
|
56
56
|
RuntimeContext* get_ctx(JSContext* ctx);
|
|
57
57
|
LexborDocument* get_doc(JSContext* ctx);
|
|
58
58
|
|
|
59
|
+
// Resolves the LexborDocument that owns `node`'s tree — the sandbox's
|
|
60
|
+
// primary document, or a secondary document created via
|
|
61
|
+
// DOMParser.parseFromString()/document.implementation.createHTMLDocument()
|
|
62
|
+
// (see DOMParserBindings). Any binding that *creates* a new node relative to
|
|
63
|
+
// an existing one (textContent/innerHTML setters, insertAdjacentHTML,
|
|
64
|
+
// before/after/replaceWith/append/prepend's string-to-text-node coercion,
|
|
65
|
+
// matches()/closest()) must resolve the document this way instead of using
|
|
66
|
+
// get_doc(ctx) directly — otherwise a node from a secondary document would
|
|
67
|
+
// have children created in the *primary* document's memory arena, which is
|
|
68
|
+
// undefined behavior once either document is destroyed. Falls back to the
|
|
69
|
+
// primary document if `node`'s owner isn't registered (shouldn't happen).
|
|
70
|
+
LexborDocument* doc_for_node(JSContext* ctx, void* node);
|
|
71
|
+
|
|
72
|
+
// Registers `doc` in the runtime's document registry so doc_for_node() can
|
|
73
|
+
// resolve nodes created inside it back to `doc`. Call once, right after the
|
|
74
|
+
// document is constructed/parsed.
|
|
75
|
+
void register_document(JSContext* ctx, LexborDocument* doc);
|
|
76
|
+
|
|
77
|
+
// Registers `wrapper` (the JS Document object returned to script) as the
|
|
78
|
+
// canonical JS object for `doc`, so get_document_wrapper() can hand back the
|
|
79
|
+
// same reference later (e.g. from node.ownerDocument) instead of building a
|
|
80
|
+
// new, unequal one. Call once, alongside register_document().
|
|
81
|
+
void register_document_wrapper(JSContext* ctx, LexborDocument* doc, JSValue wrapper);
|
|
82
|
+
|
|
83
|
+
// Returns the JSValue registered via register_document_wrapper() for `doc`,
|
|
84
|
+
// or JS_UNDEFINED if none was registered.
|
|
85
|
+
JSValue get_document_wrapper(JSContext* ctx, LexborDocument* doc);
|
|
86
|
+
|
|
59
87
|
// ── Node wrapper cache ────────────────────────────────────────────────────────
|
|
60
88
|
|
|
61
89
|
void invalidate_node_cache(JSContext* ctx, RuntimeContext* rctx, void* node);
|
|
@@ -75,6 +103,17 @@ using SetterFn = JSValue (*)(JSContext*, JSValue, JSValue);
|
|
|
75
103
|
|
|
76
104
|
void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter, SetterFn setter = nullptr);
|
|
77
105
|
|
|
106
|
+
// Sets the WHATWG DOM Node.ELEMENT_NODE-style numeric constants (ELEMENT_NODE,
|
|
107
|
+
// TEXT_NODE, COMMENT_NODE, ...) as read-only enumerable properties on `obj`.
|
|
108
|
+
// Values match lxb_dom_node_type_t, which already mirrors the DOM spec numbering.
|
|
109
|
+
void define_node_type_constants(JSContext* ctx, JSValue obj);
|
|
110
|
+
|
|
111
|
+
// Builds the plain {name, publicId, systemId, nodeType, nodeName} object used
|
|
112
|
+
// to represent a document's DocumentType node (see doc->doctype()). Shared by
|
|
113
|
+
// DocumentBindings (which layers its own identity cache on top) and
|
|
114
|
+
// DOMParserBindings (which doesn't cache — see its call site).
|
|
115
|
+
JSValue build_doctype_object(JSContext* ctx, LexborDocument* doc, void* doctype);
|
|
116
|
+
|
|
78
117
|
// ── Global constructor helper (instanceof support) ────────────────────────────
|
|
79
118
|
|
|
80
119
|
JSValue js_illegal_constructor(JSContext* ctx, JSValue this_val, int argc, JSValue* argv);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include "DOMBindings.hpp"
|
|
3
3
|
#include "DOMBindingsInternal.hpp"
|
|
4
4
|
#include "MutationObservers.hpp"
|
|
5
|
+
#include "bindings/EventBindings.hpp"
|
|
5
6
|
#include "../lexbor/LexborDocument.hpp"
|
|
6
7
|
#include "quickjs.h"
|
|
7
8
|
#include <stdexcept>
|
|
@@ -98,6 +99,14 @@ QuickJSRuntime::~QuickJSRuntime() {
|
|
|
98
99
|
delete stored;
|
|
99
100
|
_ctxState->doctype_wrapper = nullptr;
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
// Free cached secondary-document wrappers
|
|
104
|
+
for (auto& kv : _ctxState->document_wrappers) {
|
|
105
|
+
JSValue* stored = static_cast<JSValue*>(kv.second);
|
|
106
|
+
JS_FreeValue(ctx, *stored);
|
|
107
|
+
delete stored;
|
|
108
|
+
}
|
|
109
|
+
_ctxState->document_wrappers.clear();
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
if (_context) JS_FreeContext(reinterpret_cast<JSContext*>(_context));
|
|
@@ -243,6 +252,7 @@ void QuickJSRuntime::drainEventLoop() {
|
|
|
243
252
|
}
|
|
244
253
|
JS_FreeValue(ctx, str_val);
|
|
245
254
|
}
|
|
255
|
+
EventBindings::dispatchErrorEvent(ctx, err, ex);
|
|
246
256
|
JS_FreeValue(ctx, ex);
|
|
247
257
|
throw std::runtime_error(err);
|
|
248
258
|
}
|
|
@@ -266,6 +276,7 @@ void QuickJSRuntime::drainEventLoop() {
|
|
|
266
276
|
}
|
|
267
277
|
JS_FreeValue(ctx, str_val);
|
|
268
278
|
}
|
|
279
|
+
EventBindings::dispatchUnhandledRejectionEvent(ctx, *stored);
|
|
269
280
|
JS_FreeValue(ctx, *stored);
|
|
270
281
|
delete stored;
|
|
271
282
|
_ctxState->pending_rejection = nullptr;
|
|
@@ -394,6 +405,7 @@ void QuickJSRuntime::fireTimer(Timer* t) {
|
|
|
394
405
|
}
|
|
395
406
|
JS_FreeValue(ctx, str_val);
|
|
396
407
|
}
|
|
408
|
+
EventBindings::dispatchErrorEvent(ctx, err, ex);
|
|
397
409
|
JS_FreeValue(ctx, ex);
|
|
398
410
|
throw std::runtime_error(err);
|
|
399
411
|
}
|
|
@@ -440,6 +452,7 @@ std::string QuickJSRuntime::evaluate(const std::string& script) {
|
|
|
440
452
|
JS_FreeValue(ctx, str_val);
|
|
441
453
|
}
|
|
442
454
|
|
|
455
|
+
EventBindings::dispatchErrorEvent(ctx, err, ex);
|
|
443
456
|
JS_FreeValue(ctx, ex);
|
|
444
457
|
QJS_LOG("QuickJS exception: %s", err.c_str());
|
|
445
458
|
throw std::runtime_error(err);
|
|
@@ -39,6 +39,7 @@ struct EventListener {
|
|
|
39
39
|
void* node; // lxb_dom_node_t*
|
|
40
40
|
std::string event_type;
|
|
41
41
|
void* callback; // JSValue* (heap-allocated, owned)
|
|
42
|
+
bool is_handler_property { false };
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
// ── RuntimeContext ─────────────────────────────────────────────────────────────
|
|
@@ -75,6 +76,9 @@ struct RuntimeContext {
|
|
|
75
76
|
bool pretend_to_be_visual { false };
|
|
76
77
|
double time_origin_ms { 0 };
|
|
77
78
|
|
|
79
|
+
void* active_element { nullptr };
|
|
80
|
+
std::string ready_state { "loading" };
|
|
81
|
+
|
|
78
82
|
// node pointer → heap-allocated JSValue* (DupValue'd strong ref)
|
|
79
83
|
// Ensures the same native node always returns the same JS wrapper object.
|
|
80
84
|
std::unordered_map<void*, void*> node_wrapper_cache;
|
|
@@ -91,6 +95,30 @@ struct RuntimeContext {
|
|
|
91
95
|
// back-pointer to an attached shadow root, so we track it ourselves.
|
|
92
96
|
std::unordered_map<void*, void*> element_shadow_roots;
|
|
93
97
|
|
|
98
|
+
// Secondary documents created via DOMParser.parseFromString() /
|
|
99
|
+
// document.implementation.createHTMLDocument() (see DOMParserBindings).
|
|
100
|
+
// Owned here rather than by their JS wrapper's finalizer: QuickJS class
|
|
101
|
+
// finalizers only receive a JSRuntime*, not the JSContext needed to safely
|
|
102
|
+
// touch document_registry below, so they're freed together with the rest
|
|
103
|
+
// of the sandbox instead of individually via GC.
|
|
104
|
+
std::vector<std::unique_ptr<LexborDocument>> extra_documents;
|
|
105
|
+
|
|
106
|
+
// Raw lexbor document pointer (lxb_dom_document_t*) -> the LexborDocument
|
|
107
|
+
// wrapper that owns it (primary sandbox document, or one of
|
|
108
|
+
// extra_documents). Lets node-scoped bindings that create new nodes
|
|
109
|
+
// (textContent/innerHTML setters, insertAdjacentHTML, matches()/closest())
|
|
110
|
+
// resolve the *correct* owning document instead of assuming the sandbox's
|
|
111
|
+
// primary `document` — which would otherwise create nodes in the wrong
|
|
112
|
+
// document's memory arena when called on a DOMParser-produced element.
|
|
113
|
+
// See doc_for_node() in DOMBindingsInternal.
|
|
114
|
+
std::unordered_map<void*, LexborDocument*> document_registry;
|
|
115
|
+
|
|
116
|
+
// LexborDocument* -> its JS wrapper (JSValue*, heap-allocated, owned).
|
|
117
|
+
// Lets node.ownerDocument return the same JS object DOMParser.
|
|
118
|
+
// parseFromString()/createHTMLDocument() already handed the caller,
|
|
119
|
+
// instead of building a second, unequal wrapper for the same document.
|
|
120
|
+
std::unordered_map<LexborDocument*, void*> document_wrappers;
|
|
121
|
+
|
|
94
122
|
~RuntimeContext() = default;
|
|
95
123
|
};
|
|
96
124
|
|
|
@@ -86,6 +86,20 @@ const char* kAbortBootstrapScript = R"JS(
|
|
|
86
86
|
setTimeout(function() { s._abort(makeTimeoutError()); }, ms);
|
|
87
87
|
return s;
|
|
88
88
|
};
|
|
89
|
+
AbortSignal.any = function(signals) {
|
|
90
|
+
var s = new AbortSignal();
|
|
91
|
+
var list = [];
|
|
92
|
+
for (var i = 0; i < signals.length; i++) list.push(signals[i]);
|
|
93
|
+
for (var j = 0; j < list.length; j++) {
|
|
94
|
+
if (list[j].aborted) { s._abort(list[j].reason); break; }
|
|
95
|
+
}
|
|
96
|
+
if (!s.aborted) {
|
|
97
|
+
list.forEach(function(signal) {
|
|
98
|
+
signal.addEventListener('abort', function() { s._abort(signal.reason); });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return s;
|
|
102
|
+
};
|
|
89
103
|
|
|
90
104
|
globalThis.AbortSignal = AbortSignal;
|
|
91
105
|
|
|
@@ -20,6 +20,16 @@ const char* kBlobBootstrapScript = R"JS(
|
|
|
20
20
|
return Array.prototype.slice.call(new TextEncoder().encode(String(part)));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function normalizeBlobType(type) {
|
|
24
|
+
if (type === undefined || type === null) return '';
|
|
25
|
+
var s = String(type);
|
|
26
|
+
for (var i = 0; i < s.length; i++) {
|
|
27
|
+
var code = s.charCodeAt(i);
|
|
28
|
+
if (code < 0x20 || code > 0x7e) return '';
|
|
29
|
+
}
|
|
30
|
+
return s.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
function Blob(parts, options) {
|
|
24
34
|
var bytes = [];
|
|
25
35
|
if (parts) {
|
|
@@ -29,7 +39,7 @@ const char* kBlobBootstrapScript = R"JS(
|
|
|
29
39
|
}
|
|
30
40
|
this._bytes = bytes;
|
|
31
41
|
this.size = bytes.length;
|
|
32
|
-
this.type = (options && options.type)
|
|
42
|
+
this.type = normalizeBlobType(options && options.type);
|
|
33
43
|
}
|
|
34
44
|
Blob.prototype.slice = function(start, end, contentType) {
|
|
35
45
|
var sliced = this._bytes.slice(start, end);
|
|
@@ -55,6 +65,23 @@ const char* kBlobBootstrapScript = R"JS(
|
|
|
55
65
|
};
|
|
56
66
|
globalThis.Blob = Blob;
|
|
57
67
|
|
|
68
|
+
function File(parts, name, options) {
|
|
69
|
+
Blob.call(this, parts, options);
|
|
70
|
+
var lastModified;
|
|
71
|
+
if (options && options.lastModified !== undefined) {
|
|
72
|
+
var n = Number(options.lastModified);
|
|
73
|
+
lastModified = (isNaN(n) || !isFinite(n)) ? 0 : Math.trunc(n);
|
|
74
|
+
} else {
|
|
75
|
+
lastModified = Date.now();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Object.defineProperty(this, 'name', { value: String(name), enumerable: true, configurable: true });
|
|
79
|
+
Object.defineProperty(this, 'lastModified', { value: lastModified, enumerable: true, configurable: true });
|
|
80
|
+
}
|
|
81
|
+
File.prototype = Object.create(Blob.prototype);
|
|
82
|
+
File.prototype.constructor = File;
|
|
83
|
+
globalThis.File = File;
|
|
84
|
+
|
|
58
85
|
function FileReader() {
|
|
59
86
|
this.readyState = 0;
|
|
60
87
|
this.result = null;
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
namespace margelo::nitro::nitrojsdom {
|
|
6
6
|
|
|
7
|
-
// Registers globalThis.Blob and globalThis.FileReader.
|
|
8
|
-
// TextEncoder/TextDecoder and btoa, so must run after
|
|
9
|
-
// and WindowBindings.
|
|
7
|
+
// Registers globalThis.Blob, globalThis.File, and globalThis.FileReader.
|
|
8
|
+
// Pure JS on top of TextEncoder/TextDecoder and btoa, so must run after
|
|
9
|
+
// TextEncodingBindings and WindowBindings.
|
|
10
10
|
struct BlobBindings {
|
|
11
11
|
static void install(JSContext* ctx);
|
|
12
12
|
};
|
|
@@ -227,6 +227,52 @@ const char* kCSSOMBootstrapScript = R"JS(
|
|
|
227
227
|
},
|
|
228
228
|
configurable: true,
|
|
229
229
|
});
|
|
230
|
+
|
|
231
|
+
// ── getComputedStyle ──────────────────────────────────────────────────
|
|
232
|
+
// No layout/cascade engine backs this sandbox — there is no stylesheet
|
|
233
|
+
// specificity resolution or inheritance, only the element's own inline
|
|
234
|
+
// `style` attribute. `display` falls back to a small user-agent-stylesheet
|
|
235
|
+
// approximation (block/inline/inline-block/none by tag name, or 'none' for
|
|
236
|
+
// a `hidden` attribute) since "is this element visible" is the one
|
|
237
|
+
// getComputedStyle check real-world embedded scripts actually make;
|
|
238
|
+
// `visibility`/`opacity` fall back to their initial values. Every other
|
|
239
|
+
// unset property returns '', not a computed initial value.
|
|
240
|
+
var kBlockTags = { ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DETAILS: 1, DIALOG: 1, DD: 1, DIV: 1,
|
|
241
|
+
DL: 1, DT: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1, FOOTER: 1, FORM: 1, H1: 1, H2: 1, H3: 1, H4: 1,
|
|
242
|
+
H5: 1, H6: 1, HEADER: 1, HGROUP: 1, HR: 1, LI: 1, MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1, SECTION: 1,
|
|
243
|
+
TABLE: 1, UL: 1, HTML: 1, BODY: 1 };
|
|
244
|
+
var kNoneTags = { SCRIPT: 1, STYLE: 1, HEAD: 1, TITLE: 1, META: 1, LINK: 1, TEMPLATE: 1 };
|
|
245
|
+
var kInlineBlockTags = { IMG: 1, BUTTON: 1, INPUT: 1, SELECT: 1, TEXTAREA: 1 };
|
|
246
|
+
|
|
247
|
+
function defaultDisplay(tagName) {
|
|
248
|
+
if (kNoneTags[tagName]) return 'none';
|
|
249
|
+
if (kInlineBlockTags[tagName]) return 'inline-block';
|
|
250
|
+
if (kBlockTags[tagName]) return 'block';
|
|
251
|
+
return 'inline';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveProperty(el, kebabName) {
|
|
255
|
+
var v = el.style.getPropertyValue(kebabName);
|
|
256
|
+
if (v) return v;
|
|
257
|
+
if (kebabName === 'display') return el.hasAttribute('hidden') ? 'none' : defaultDisplay(el.tagName);
|
|
258
|
+
if (kebabName === 'visibility') return 'visible';
|
|
259
|
+
if (kebabName === 'opacity') return '1';
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
globalThis.getComputedStyle = function(el) {
|
|
264
|
+
if (!el || typeof el.tagName !== 'string') {
|
|
265
|
+
throw new TypeError("Failed to execute 'getComputedStyle': parameter 1 is not of type 'Element'.");
|
|
266
|
+
}
|
|
267
|
+
return new Proxy({}, {
|
|
268
|
+
get: function(_target, prop) {
|
|
269
|
+
if (prop === 'getPropertyValue') return function(name) { return resolveProperty(el, String(name)); };
|
|
270
|
+
if (prop === 'getPropertyPriority') return function() { return ''; };
|
|
271
|
+
if (typeof prop !== 'string') return undefined;
|
|
272
|
+
return resolveProperty(el, camelToKebab(prop));
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
};
|
|
230
276
|
})();
|
|
231
277
|
)JS";
|
|
232
278
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
namespace margelo::nitro::nitrojsdom {
|
|
6
6
|
|
|
7
7
|
// Registers a minimal CSSOM: globalThis.CSSRule/CSSStyleRule/CSSStyleSheet,
|
|
8
|
-
// Element.prototype.sheet (tag-checked for "style"),
|
|
8
|
+
// Element.prototype.sheet (tag-checked for "style"), document.styleSheets,
|
|
9
|
+
// and globalThis.getComputedStyle (inline-style + tag-default-display only,
|
|
10
|
+
// no real cascade — see the bootstrap script for the exact fallback rules).
|
|
9
11
|
//
|
|
10
12
|
// Rules are split by a hand-rolled top-level brace scanner rather than
|
|
11
13
|
// Lexbor's CSS rule/declaration AST (lxb_css_rule_t) — mirrors how
|