@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.
Files changed (35) hide show
  1. package/android/CMakeLists.txt +4 -0
  2. package/cpp/HybridHtmlSandbox.cpp +7 -3
  3. package/cpp/lexbor/LexborDocument.cpp +62 -0
  4. package/cpp/lexbor/LexborDocument.hpp +14 -0
  5. package/cpp/quickjs/DOMBindings.cpp +9 -1
  6. package/cpp/quickjs/DOMBindingsInternal.cpp +67 -0
  7. package/cpp/quickjs/DOMBindingsInternal.hpp +39 -0
  8. package/cpp/quickjs/QuickJSRuntime.cpp +13 -0
  9. package/cpp/quickjs/QuickJSRuntime.hpp +28 -0
  10. package/cpp/quickjs/bindings/AbortBindings.cpp +14 -0
  11. package/cpp/quickjs/bindings/BlobBindings.cpp +28 -1
  12. package/cpp/quickjs/bindings/BlobBindings.hpp +3 -3
  13. package/cpp/quickjs/bindings/CSSOMBindings.cpp +46 -0
  14. package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
  15. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  16. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  17. package/cpp/quickjs/bindings/DocumentBindings.cpp +166 -11
  18. package/cpp/quickjs/bindings/ElementBindings.cpp +296 -20
  19. package/cpp/quickjs/bindings/EventBindings.cpp +277 -5
  20. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  21. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  22. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  23. package/cpp/quickjs/bindings/FetchBindings.cpp +43 -8
  24. package/cpp/quickjs/bindings/FormBindings.cpp +223 -0
  25. package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
  26. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  27. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  28. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +45 -0
  29. package/cpp/quickjs/bindings/TimerBindings.cpp +79 -0
  30. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  31. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  32. package/cpp/quickjs/bindings/UrlBindings.cpp +9 -0
  33. package/cpp/quickjs/bindings/WindowBindings.cpp +286 -4
  34. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  35. package/package.json +17 -2
@@ -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
- "document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true, cancelable: true }));"
61
- "document.dispatchEvent(new Event('load'));");
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
- (void)document;
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) ? String(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. Pure JS on top of
8
- // TextEncoder/TextDecoder and btoa, so must run after TextEncodingBindings
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"), and document.styleSheets.
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