@salve-software/react-native-nitro-jsdom 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/android/CMakeLists.txt +6 -0
  2. package/cpp/HybridHtmlSandbox.cpp +14 -7
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +65 -3
  5. package/cpp/lexbor/LexborDocument.hpp +17 -2
  6. package/cpp/quickjs/DOMBindings.cpp +14 -1
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +67 -0
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +39 -0
  9. package/cpp/quickjs/QuickJSRuntime.cpp +13 -0
  10. package/cpp/quickjs/QuickJSRuntime.hpp +29 -0
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +14 -0
  12. package/cpp/quickjs/bindings/BlobBindings.cpp +28 -1
  13. package/cpp/quickjs/bindings/BlobBindings.hpp +3 -3
  14. package/cpp/quickjs/bindings/CSSOMBindings.cpp +96 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
  16. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +1 -1
  17. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  18. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  19. package/cpp/quickjs/bindings/DocumentBindings.cpp +174 -11
  20. package/cpp/quickjs/bindings/ElementBindings.cpp +516 -20
  21. package/cpp/quickjs/bindings/EventBindings.cpp +281 -5
  22. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  23. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  24. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  25. package/cpp/quickjs/bindings/FetchBindings.cpp +43 -8
  26. package/cpp/quickjs/bindings/FormBindings.cpp +294 -0
  27. package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
  28. package/cpp/quickjs/bindings/IntlBindings.cpp +352 -0
  29. package/cpp/quickjs/bindings/IntlBindings.hpp +29 -0
  30. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  31. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  32. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +45 -0
  33. package/cpp/quickjs/bindings/TimerBindings.cpp +79 -0
  34. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  35. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  36. package/cpp/quickjs/bindings/UrlBindings.cpp +43 -0
  37. package/cpp/quickjs/bindings/WindowBindings.cpp +295 -4
  38. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  39. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp +241 -0
  40. package/cpp/quickjs/bindings/WindowNamedPropertiesBindings.hpp +30 -0
  41. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +2 -1
  42. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  43. package/lib/module/classes/JSDOM/JSDOM.class.js +2 -1
  44. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  45. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  46. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +5 -4
  47. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -1
  48. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  49. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  50. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  51. package/package.json +17 -2
  52. package/src/classes/JSDOM/JSDOM.class.ts +2 -1
  53. package/src/classes/JSDOM/types/IJSDOMOptions.ts +5 -4
  54. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -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
@@ -60,6 +61,7 @@ add_library(${PACKAGE_NAME} SHARED
60
61
  ../cpp/quickjs/bindings/UrlBindings.cpp
61
62
  ../cpp/quickjs/bindings/AbortBindings.cpp
62
63
  ../cpp/quickjs/bindings/TextEncodingBindings.cpp
64
+ ../cpp/quickjs/bindings/IntlBindings.cpp
63
65
  ../cpp/quickjs/bindings/FormBindings.cpp
64
66
  ../cpp/quickjs/bindings/BlobBindings.cpp
65
67
  ../cpp/quickjs/bindings/CSSOMBindings.cpp
@@ -69,6 +71,10 @@ add_library(${PACKAGE_NAME} SHARED
69
71
  ../cpp/quickjs/bindings/TemplateBindings.cpp
70
72
  ../cpp/quickjs/bindings/SlotBindings.cpp
71
73
  ../cpp/quickjs/bindings/XmlSerializerBindings.cpp
74
+ ../cpp/quickjs/bindings/LayoutStubBindings.cpp
75
+ ../cpp/quickjs/bindings/TreeWalkerBindings.cpp
76
+ ../cpp/quickjs/bindings/EventTargetBindings.cpp
77
+ ../cpp/quickjs/bindings/WindowNamedPropertiesBindings.cpp
72
78
  ${QUICKJS_SOURCES}
73
79
  )
74
80
 
@@ -43,10 +43,13 @@ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, con
43
43
  _runtime->bindDocument(_document.get());
44
44
 
45
45
  if (runScripts) {
46
- for (const auto& script : _document->getScriptContents()) {
46
+ auto* rctx = _runtime->contextState();
47
+ for (const auto& [scriptEl, script] : _document->getScriptContents()) {
48
+ rctx->current_script = scriptEl;
47
49
  try {
48
50
  _runtime->evaluate(script);
49
51
  } catch (...) { }
52
+ rctx->current_script = nullptr;
50
53
  }
51
54
  }
52
55
 
@@ -55,10 +58,14 @@ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, con
55
58
  // or window.addEventListener(...) — the common embedded-widget bootstrap
56
59
  // pattern — see them. There's no real subresource loading in this sandbox,
57
60
  // so both fire back-to-back with nothing in between.
61
+ _runtime->contextState()->ready_state = "interactive";
58
62
  try {
59
- _runtime->evaluate(
60
- "document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true, cancelable: true }));"
61
- "document.dispatchEvent(new Event('load'));");
63
+ _runtime->evaluate("document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true }));");
64
+ } catch (...) { }
65
+
66
+ _runtime->contextState()->ready_state = "complete";
67
+ try {
68
+ _runtime->evaluate("document.dispatchEvent(new Event('load'));");
62
69
  } catch (...) { }
63
70
 
64
71
  _initialized = true;
@@ -107,7 +114,7 @@ void HybridHtmlSandbox::setConsoleCallback(
107
114
 
108
115
  void HybridHtmlSandbox::setDialogCallbacks(
109
116
  const std::optional<std::variant<nitro::NullType, std::function<void(const std::string&)>>>& onAlert,
110
- const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>>& onConfirm,
117
+ const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>>& onConfirm,
111
118
  const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string&, const std::optional<std::string>&)>>>& onPrompt) {
112
119
  if (!_runtime) return;
113
120
 
@@ -125,10 +132,10 @@ void HybridHtmlSandbox::setDialogCallbacks(
125
132
  if (!onConfirm.has_value() || std::holds_alternative<nitro::NullType>(onConfirm.value())) {
126
133
  _runtime->setConfirmCallback(nullptr);
127
134
  } else {
128
- auto fn = std::get<std::function<std::shared_ptr<Promise<bool>>(const std::string&)>>(onConfirm.value());
135
+ auto fn = std::get<std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string&)>>(onConfirm.value());
129
136
  _runtime->setConfirmCallback([fn](const std::string& message) -> bool {
130
137
  try {
131
- return fn(message)->await().get();
138
+ return fn(message)->await().get()->await().get();
132
139
  } catch (...) {
133
140
  return false;
134
141
  }
@@ -30,7 +30,7 @@ public:
30
30
 
31
31
  void setDialogCallbacks(
32
32
  const std::optional<std::variant<nitro::NullType, std::function<void(const std::string& /* message */)>>>& onAlert,
33
- const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<bool>>(const std::string& /* message */)>>>& onConfirm,
33
+ const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::shared_ptr<Promise<bool>>>>(const std::string& /* message */)>>>& onConfirm,
34
34
  const std::optional<std::variant<nitro::NullType, std::function<std::shared_ptr<Promise<std::variant<nitro::NullType, std::string>>>(const std::string& /* message */, const std::optional<std::string>& /* defaultValue */)>>>& onPrompt
35
35
  ) override;
36
36
 
@@ -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>
@@ -196,9 +199,9 @@ void* LexborDocument::documentElement() const {
196
199
 
197
200
  // ── Script extraction ────────────────────────────────────────────────────────
198
201
 
199
- std::vector<std::string> LexborDocument::getScriptContents() const {
202
+ std::vector<std::pair<void*, std::string>> LexborDocument::getScriptContents() const {
200
203
  auto scriptEls = querySelectorAll_el("script");
201
- std::vector<std::string> contents;
204
+ std::vector<std::pair<void*, std::string>> contents;
202
205
  contents.reserve(scriptEls.size());
203
206
 
204
207
  for (void* el : scriptEls) {
@@ -214,7 +217,7 @@ std::vector<std::string> LexborDocument::getScriptContents() const {
214
217
  size_t len = 0;
215
218
  lxb_char_t* text = lxb_dom_node_text_content(node, &len);
216
219
  if (text && len > 0) {
217
- contents.emplace_back(reinterpret_cast<char*>(text), len);
220
+ contents.emplace_back(el, std::string(reinterpret_cast<char*>(text), len));
218
221
  lxb_dom_document_destroy_text(node->owner_document, text);
219
222
  }
220
223
  }
@@ -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));
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include <string>
4
+ #include <utility>
4
5
  #include <vector>
5
6
 
6
7
  namespace margelo::nitro::nitrojsdom {
@@ -29,8 +30,8 @@ public:
29
30
  void* head() const;
30
31
  void* documentElement() const;
31
32
 
32
- // ── Script extraction ────────────────────────────────────────────────────
33
- std::vector<std::string> getScriptContents() const;
33
+ // ── Script extraction — (element, text content) pairs; void* = lxb_dom_element_t* ──
34
+ std::vector<std::pair<void*, std::string>> getScriptContents() const;
34
35
 
35
36
  // ── Node creation ─────────────────────────────────────────────────────────
36
37
  void* createElement(const std::string& tag);
@@ -38,6 +39,20 @@ public:
38
39
  void* createComment(const std::string& text);
39
40
  void* createDocumentFragment();
40
41
 
42
+ // ── Namespaces (createElementNS / Element.namespaceURI) ───────────────────
43
+ // qualifiedName may be "prefix:localName" or a bare "localName". nsUri may be
44
+ // empty for the null namespace. Lexbor registers arbitrary namespace URIs
45
+ // dynamically (lxb_ns_append), so any URI works, not just HTML/SVG/MathML.
46
+ // Note: lxb_tag_append_lower lowercases local_name internally, so
47
+ // mixed-case foreign tag/attribute names (e.g. SVG's "viewBox",
48
+ // "linearGradient") come back lowercased — a lexbor limitation, not
49
+ // something this binding can work around.
50
+ void* createElementNS(const std::string& nsUri, const std::string& qualifiedName);
51
+
52
+ // Returns the namespace URI for a node, or "" if the node has no namespace
53
+ // (LXB_NS__UNDEF) — callers should map "" to JS null.
54
+ std::string namespaceURI(void* node) const;
55
+
41
56
  // ── Shadow DOM ─────────────────────────────────────────────────────────────
42
57
  // mode: 0 = open, 1 = closed (matches lxb_dom_shadow_root_mode_t).
43
58
  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"
@@ -15,6 +16,7 @@
15
16
  #include "bindings/UrlBindings.hpp"
16
17
  #include "bindings/AbortBindings.hpp"
17
18
  #include "bindings/TextEncodingBindings.hpp"
19
+ #include "bindings/IntlBindings.hpp"
18
20
  #include "bindings/FormBindings.hpp"
19
21
  #include "bindings/BlobBindings.hpp"
20
22
  #include "bindings/CSSOMBindings.hpp"
@@ -24,6 +26,10 @@
24
26
  #include "bindings/TemplateBindings.hpp"
25
27
  #include "bindings/SlotBindings.hpp"
26
28
  #include "bindings/XmlSerializerBindings.hpp"
29
+ #include "bindings/LayoutStubBindings.hpp"
30
+ #include "bindings/TreeWalkerBindings.hpp"
31
+ #include "bindings/EventTargetBindings.hpp"
32
+ #include "bindings/WindowNamedPropertiesBindings.hpp"
27
33
  #include <lexbor/html/html.h>
28
34
  #include <lexbor/dom/dom.h>
29
35
 
@@ -36,13 +42,14 @@ namespace margelo::nitro::nitrojsdom {
36
42
 
37
43
  void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
38
44
  JSContext* ctx = static_cast<JSContext*>(runtime->context());
39
- (void)document;
45
+ register_document(ctx, document); // so doc_for_node() resolves nodes back to the primary document
40
46
 
41
47
  DOMExceptionBindings::install(ctx); // no dependencies; other modules throw DOMException instances
42
48
  LiveCollectionBindings::install(ctx);
43
49
  ElementBindings::install(ctx); // registers the Element class + proto
44
50
  ShadowRootBindings::install(ctx); // needs Element's proto + js_node_class_id's proto
45
51
  DocumentBindings::install(ctx); // creates globalThis.document
52
+ DOMParserBindings::install(ctx); // adds document.implementation + globalThis.DOMParser; needs globalThis.document
46
53
  CookieBindings::install(ctx); // needs globalThis.document to exist
47
54
  TemplateBindings::install(ctx); // needs Element's proto
48
55
  CustomElementsBindings::install(ctx); // needs Element/ShadowRoot protos + globalThis.document
@@ -53,11 +60,15 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
53
60
  UrlBindings::install(ctx);
54
61
  AbortBindings::install(ctx);
55
62
  TextEncodingBindings::install(ctx);
63
+ IntlBindings::install(ctx);
56
64
  BlobBindings::install(ctx); // uses TextEncoder/TextDecoder + btoa, so must run after both
57
65
  FetchBindings::install(ctx); // XHR bootstrap uses `new Event(...)`, so must run after EventBindings
58
66
  FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
59
67
  SlotBindings::install(ctx); // uses Event/dispatchEvent + Element/ShadowRoot protos, so must run after EventBindings/ShadowRootBindings
60
68
  XmlSerializerBindings::install(ctx); // pure serialization, no ordering requirement beyond ElementBindings
69
+ LayoutStubBindings::install(ctx); // needs Element's proto + globalThis.document to exist
70
+ TreeWalkerBindings::install(ctx); // needs Element's proto (Node traversal props) + globalThis.document to exist
71
+ EventTargetBindings::install(ctx); // no dependencies
61
72
 
62
73
  // ── localStorage / sessionStorage ──────────────────────────────────────────
63
74
  {
@@ -80,6 +91,8 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
80
91
  rctx->mutation_observers->install(ctx, doc_node);
81
92
  }
82
93
  }
94
+
95
+ WindowNamedPropertiesBindings::install(ctx); // must run last, after localStorage/sessionStorage exist
83
96
  }
84
97
 
85
98
  } // namespace margelo::nitro::nitrojsdom
@@ -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,10 @@ 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
+ void* current_script { nullptr };
81
+ std::string ready_state { "loading" };
82
+
78
83
  // node pointer → heap-allocated JSValue* (DupValue'd strong ref)
79
84
  // Ensures the same native node always returns the same JS wrapper object.
80
85
  std::unordered_map<void*, void*> node_wrapper_cache;
@@ -91,6 +96,30 @@ struct RuntimeContext {
91
96
  // back-pointer to an attached shadow root, so we track it ourselves.
92
97
  std::unordered_map<void*, void*> element_shadow_roots;
93
98
 
99
+ // Secondary documents created via DOMParser.parseFromString() /
100
+ // document.implementation.createHTMLDocument() (see DOMParserBindings).
101
+ // Owned here rather than by their JS wrapper's finalizer: QuickJS class
102
+ // finalizers only receive a JSRuntime*, not the JSContext needed to safely
103
+ // touch document_registry below, so they're freed together with the rest
104
+ // of the sandbox instead of individually via GC.
105
+ std::vector<std::unique_ptr<LexborDocument>> extra_documents;
106
+
107
+ // Raw lexbor document pointer (lxb_dom_document_t*) -> the LexborDocument
108
+ // wrapper that owns it (primary sandbox document, or one of
109
+ // extra_documents). Lets node-scoped bindings that create new nodes
110
+ // (textContent/innerHTML setters, insertAdjacentHTML, matches()/closest())
111
+ // resolve the *correct* owning document instead of assuming the sandbox's
112
+ // primary `document` — which would otherwise create nodes in the wrong
113
+ // document's memory arena when called on a DOMParser-produced element.
114
+ // See doc_for_node() in DOMBindingsInternal.
115
+ std::unordered_map<void*, LexborDocument*> document_registry;
116
+
117
+ // LexborDocument* -> its JS wrapper (JSValue*, heap-allocated, owned).
118
+ // Lets node.ownerDocument return the same JS object DOMParser.
119
+ // parseFromString()/createHTMLDocument() already handed the caller,
120
+ // instead of building a second, unequal wrapper for the same document.
121
+ std::unordered_map<LexborDocument*, void*> document_wrappers;
122
+
94
123
  ~RuntimeContext() = default;
95
124
  };
96
125
 
@@ -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
  };