@salve-software/react-native-nitro-jsdom 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/android/CMakeLists.txt +18 -0
  2. package/cpp/HybridHtmlSandbox.cpp +20 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +279 -67
  5. package/cpp/lexbor/LexborDocument.hpp +43 -10
  6. package/cpp/quickjs/DOMBindings.cpp +37 -1
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +101 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +56 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +32 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +50 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +128 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +164 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +290 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +25 -0
  17. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  18. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  19. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  20. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  22. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  24. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  25. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  26. package/cpp/quickjs/bindings/DocumentBindings.cpp +239 -4
  27. package/cpp/quickjs/bindings/ElementBindings.cpp +556 -44
  28. package/cpp/quickjs/bindings/EventBindings.cpp +414 -8
  29. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  30. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  31. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  32. package/cpp/quickjs/bindings/FetchBindings.cpp +45 -6
  33. package/cpp/quickjs/bindings/FormBindings.cpp +365 -0
  34. package/cpp/quickjs/bindings/FormBindings.hpp +20 -0
  35. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  36. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  37. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +228 -0
  38. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  39. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  40. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  41. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  42. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  43. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  44. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  45. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  46. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  47. package/cpp/quickjs/bindings/TimerBindings.cpp +120 -0
  48. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  49. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  50. package/cpp/quickjs/bindings/UrlBindings.cpp +295 -0
  51. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  52. package/cpp/quickjs/bindings/WindowBindings.cpp +552 -0
  53. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  54. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  55. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  56. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  57. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  58. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  59. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  60. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  61. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  62. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  63. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  64. package/package.json +17 -2
  65. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  66. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -48,13 +48,31 @@ add_library(${PACKAGE_NAME} SHARED
48
48
  ../cpp/quickjs/Storage.cpp
49
49
  ../cpp/quickjs/bindings/ClassListBindings.cpp
50
50
  ../cpp/quickjs/bindings/DatasetBindings.cpp
51
+ ../cpp/quickjs/bindings/DOMExceptionBindings.cpp
51
52
  ../cpp/quickjs/bindings/StyleBindings.cpp
52
53
  ../cpp/quickjs/bindings/ElementBindings.cpp
53
54
  ../cpp/quickjs/bindings/DocumentBindings.cpp
55
+ ../cpp/quickjs/bindings/DOMParserBindings.cpp
54
56
  ../cpp/quickjs/bindings/EventBindings.cpp
55
57
  ../cpp/quickjs/bindings/TimerBindings.cpp
56
58
  ../cpp/quickjs/bindings/WindowBindings.cpp
57
59
  ../cpp/quickjs/bindings/FetchBindings.cpp
60
+ ../cpp/quickjs/bindings/LiveCollectionBindings.cpp
61
+ ../cpp/quickjs/bindings/UrlBindings.cpp
62
+ ../cpp/quickjs/bindings/AbortBindings.cpp
63
+ ../cpp/quickjs/bindings/TextEncodingBindings.cpp
64
+ ../cpp/quickjs/bindings/FormBindings.cpp
65
+ ../cpp/quickjs/bindings/BlobBindings.cpp
66
+ ../cpp/quickjs/bindings/CSSOMBindings.cpp
67
+ ../cpp/quickjs/bindings/ShadowRootBindings.cpp
68
+ ../cpp/quickjs/bindings/CustomElementsBindings.cpp
69
+ ../cpp/quickjs/bindings/CookieBindings.cpp
70
+ ../cpp/quickjs/bindings/TemplateBindings.cpp
71
+ ../cpp/quickjs/bindings/SlotBindings.cpp
72
+ ../cpp/quickjs/bindings/XmlSerializerBindings.cpp
73
+ ../cpp/quickjs/bindings/LayoutStubBindings.cpp
74
+ ../cpp/quickjs/bindings/TreeWalkerBindings.cpp
75
+ ../cpp/quickjs/bindings/EventTargetBindings.cpp
58
76
  ${QUICKJS_SOURCES}
59
77
  )
60
78
 
@@ -34,20 +34,37 @@ std::string jsonQuote(const std::string& s) {
34
34
  }
35
35
  } // namespace
36
36
 
37
- void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, const std::string& url) {
37
+ void HybridHtmlSandbox::initialize(const std::string& html, bool runScripts, const std::string& url, bool pretendToBeVisual) {
38
38
  _document = std::make_unique<LexborDocument>();
39
39
  _document->parse(html);
40
40
 
41
41
  _runtime = std::make_unique<QuickJSRuntime>();
42
- _runtime->initialize(url);
42
+ _runtime->initialize(url, pretendToBeVisual);
43
43
  _runtime->bindDocument(_document.get());
44
44
 
45
45
  if (runScripts) {
46
46
  for (const auto& script : _document->getScriptContents()) {
47
- _runtime->evaluate(script);
47
+ try {
48
+ _runtime->evaluate(script);
49
+ } catch (...) { }
48
50
  }
49
51
  }
50
52
 
53
+ // Fire once parsing (and any inline <script> execution above) is done, so
54
+ // scripts that do document.addEventListener('DOMContentLoaded'/'load', ...)
55
+ // or window.addEventListener(...) — the common embedded-widget bootstrap
56
+ // pattern — see them. There's no real subresource loading in this sandbox,
57
+ // so both fire back-to-back with nothing in between.
58
+ _runtime->contextState()->ready_state = "interactive";
59
+ try {
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'));");
66
+ } catch (...) { }
67
+
51
68
  _initialized = true;
52
69
  }
53
70
 
@@ -18,7 +18,7 @@ public:
18
18
  HybridHtmlSandbox() : HybridObject(TAG), HybridHtmlSandboxSpec() {}
19
19
  ~HybridHtmlSandbox() override = default;
20
20
 
21
- void initialize(const std::string& html, bool runScripts, const std::string& url) override;
21
+ void initialize(const std::string& html, bool runScripts, const std::string& url, bool pretendToBeVisual) override;
22
22
 
23
23
  std::shared_ptr<Promise<std::string>> evaluate(const std::string& script) override;
24
24
 
@@ -1,9 +1,20 @@
1
1
  #include "LexborDocument.hpp"
2
2
  #include <lexbor/html/html.h>
3
+ #include <lexbor/html/interfaces/template_element.h>
3
4
  #include <lexbor/css/css.h>
4
5
  #include <lexbor/selectors/selectors.h>
5
6
  #include <lexbor/dom/dom.h>
7
+ #include <lexbor/dom/interfaces/character_data.h>
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>
6
12
  #include <stdexcept>
13
+ #include <cctype>
14
+ #include <cstring>
15
+ #include <unordered_set>
16
+ #include <vector>
17
+ #include <algorithm>
7
18
 
8
19
  namespace margelo::nitro::nitrojsdom {
9
20
 
@@ -22,6 +33,26 @@ static std::string serializeNode(lxb_dom_node_t* node) {
22
33
  return result;
23
34
  }
24
35
 
36
+ bool is_javascript_mime_type(const std::string& type) {
37
+ if (type.empty()) return true;
38
+ std::string t;
39
+ t.reserve(type.size());
40
+ for (char c : type) t += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
41
+ size_t start = t.find_first_not_of(" \t\n\r\f");
42
+ if (start == std::string::npos) return true;
43
+ size_t end = t.find_last_not_of(" \t\n\r\f");
44
+ t = t.substr(start, end - start + 1);
45
+
46
+ static const std::unordered_set<std::string> kJsTypes = {
47
+ "text/javascript", "application/javascript", "application/ecmascript",
48
+ "application/x-ecmascript", "application/x-javascript", "text/ecmascript",
49
+ "text/javascript1.0", "text/javascript1.1", "text/javascript1.2",
50
+ "text/javascript1.3", "text/javascript1.4", "text/javascript1.5",
51
+ "text/jscript", "text/livescript", "text/x-ecmascript", "text/x-javascript",
52
+ };
53
+ return kJsTypes.count(t) > 0;
54
+ }
55
+
25
56
  struct FindFirstCtx { lxb_dom_element_t* result = nullptr; };
26
57
 
27
58
  static lxb_status_t findFirstCallback(lxb_dom_node_t* node, lxb_css_selector_specificity_t, void* ctx) {
@@ -29,11 +60,6 @@ static lxb_status_t findFirstCallback(lxb_dom_node_t* node, lxb_css_selector_spe
29
60
  return LXB_STATUS_STOP;
30
61
  }
31
62
 
32
- static lxb_status_t findAllStringCallback(lxb_dom_node_t* node, lxb_css_selector_specificity_t, void* ctx) {
33
- static_cast<std::vector<std::string>*>(ctx)->push_back(serializeNode(node));
34
- return LXB_STATUS_OK;
35
- }
36
-
37
63
  static lxb_status_t findAllPtrCallback(lxb_dom_node_t* node, lxb_css_selector_specificity_t, void* ctx) {
38
64
  static_cast<std::vector<void*>*>(ctx)->push_back(lxb_dom_interface_element(node));
39
65
  return LXB_STATUS_OK;
@@ -134,14 +160,14 @@ std::vector<void*> LexborDocument::querySelectorAll_el(const std::string& sel) c
134
160
 
135
161
  void* LexborDocument::querySelectorFromEl(void* element, const std::string& sel) const {
136
162
  if (!element) return nullptr;
137
- return findFirstFrom(lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(element)), sel);
163
+ return findFirstFrom(static_cast<lxb_dom_node_t*>(element), sel);
138
164
  }
139
165
 
140
166
  std::vector<void*> LexborDocument::querySelectorAllFromEl(void* element, const std::string& sel) const {
141
167
  if (!_document || !element) return {};
142
168
  auto* parser = static_cast<lxb_css_parser_t*>(_cssParser);
143
169
  auto* selectors = static_cast<lxb_selectors_t*>(_selectors);
144
- auto* node = lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(element));
170
+ auto* node = static_cast<lxb_dom_node_t*>(element);
145
171
 
146
172
  lxb_css_selector_list_t* list = lxb_css_selectors_parse(parser,
147
173
  reinterpret_cast<const lxb_char_t*>(sel.data()), sel.size());
@@ -179,7 +205,15 @@ std::vector<std::string> LexborDocument::getScriptContents() const {
179
205
  contents.reserve(scriptEls.size());
180
206
 
181
207
  for (void* el : scriptEls) {
182
- auto* node = lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(el));
208
+ auto* element = static_cast<lxb_dom_element_t*>(el);
209
+
210
+ size_t type_len = 0;
211
+ const lxb_char_t* type_attr = lxb_dom_element_get_attribute(element,
212
+ reinterpret_cast<const lxb_char_t*>("type"), 4, &type_len);
213
+ std::string type = type_attr ? std::string(reinterpret_cast<const char*>(type_attr), type_len) : "";
214
+ if (!is_javascript_mime_type(type)) continue;
215
+
216
+ auto* node = lxb_dom_interface_node(element);
183
217
  size_t len = 0;
184
218
  lxb_char_t* text = lxb_dom_node_text_content(node, &len);
185
219
  if (text && len > 0) {
@@ -199,6 +233,65 @@ void* LexborDocument::createElement(const std::string& tag) {
199
233
  reinterpret_cast<const lxb_char_t*>(tag.data()), tag.size(), nullptr);
200
234
  }
201
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
+
202
295
  void* LexborDocument::createTextNode(const std::string& text) {
203
296
  if (!_document) return nullptr;
204
297
  auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
@@ -219,6 +312,16 @@ void* LexborDocument::createDocumentFragment() {
219
312
  return lxb_dom_document_create_document_fragment(dom_doc);
220
313
  }
221
314
 
315
+ void* LexborDocument::createShadowRoot(void* hostElement, int mode) {
316
+ if (!_document || !hostElement) return nullptr;
317
+ auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
318
+ auto* shadow = lxb_dom_shadow_root_interface_create(dom_doc);
319
+ if (!shadow) return nullptr;
320
+ shadow->host = static_cast<lxb_dom_element_t*>(hostElement);
321
+ shadow->mode = static_cast<lxb_dom_shadow_root_mode_t>(mode);
322
+ return shadow;
323
+ }
324
+
222
325
  // ── Element content ──────────────────────────────────────────────────────────
223
326
 
224
327
  void LexborDocument::setTextContentOnEl(void* element, const std::string& text) {
@@ -242,10 +345,40 @@ void LexborDocument::setTextContentOnEl(void* element, const std::string& text)
242
345
  }
243
346
  }
244
347
 
348
+ void LexborDocument::setInnerHTMLOnShadowRoot(void* shadowRoot, void* hostElement, const std::string& html) {
349
+ auto* host = static_cast<lxb_dom_element_t*>(hostElement);
350
+ auto* doc = static_cast<lxb_html_document_t*>(_document);
351
+ auto* root = static_cast<lxb_dom_node_t*>(shadowRoot);
352
+
353
+ lxb_dom_node_t* frag = lxb_html_document_parse_fragment(doc, host,
354
+ reinterpret_cast<const lxb_char_t*>(html.data()), html.size());
355
+ if (!frag) return;
356
+
357
+ while (root->first_child) lxb_dom_node_destroy_deep(root->first_child);
358
+
359
+ while (frag->first_child) {
360
+ lxb_dom_node_t* child = frag->first_child;
361
+ lxb_dom_node_remove(child);
362
+ lxb_dom_node_insert_child(root, child);
363
+ }
364
+
365
+ lxb_dom_node_destroy(frag);
366
+ }
367
+
245
368
  void LexborDocument::setInnerHTMLOnEl(void* element, const std::string& html) {
246
369
  auto* el = static_cast<lxb_dom_element_t*>(element);
247
370
  auto* doc = static_cast<lxb_html_document_t*>(_document);
248
- auto* root = lxb_dom_interface_node(el);
371
+
372
+ // <template>.innerHTML populates .content (a separate inert fragment), not
373
+ // the template element's own light-DOM children — matches the parser's own
374
+ // "in template" insertion mode behavior for parsed <template> markup.
375
+ size_t tag_len = 0;
376
+ const lxb_char_t* tag_name = lxb_dom_element_local_name(el, &tag_len);
377
+ bool is_template = tag_len == 8 && memcmp(tag_name, "template", 8) == 0;
378
+
379
+ lxb_dom_node_t* root = is_template
380
+ ? lxb_dom_interface_node(reinterpret_cast<lxb_html_template_element_t*>(el)->content)
381
+ : lxb_dom_interface_node(el);
249
382
 
250
383
  lxb_dom_node_t* frag = lxb_html_document_parse_fragment(doc, el,
251
384
  reinterpret_cast<const lxb_char_t*>(html.data()), html.size());
@@ -290,76 +423,155 @@ bool LexborDocument::matchesSelector(void* element, const std::string& sel) cons
290
423
  return false;
291
424
  }
292
425
 
293
- // ── Legacy selector-based API ────────────────────────────────────────────────
426
+ // ── <template> ────────────────────────────────────────────────────────────
294
427
 
295
- std::optional<std::string> LexborDocument::querySelector(const std::string& selector) const {
296
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
297
- if (!el) return std::nullopt;
298
- return serializeNode(lxb_dom_interface_node(el));
428
+ void* LexborDocument::templateContent(void* templateEl) const {
429
+ if (!templateEl) return nullptr;
430
+ auto* tpl = reinterpret_cast<lxb_html_template_element_t*>(templateEl);
431
+ return tpl->content;
299
432
  }
300
433
 
301
- std::vector<std::string> LexborDocument::querySelectorAll(const std::string& selector) const {
302
- if (!_document) return {};
303
- auto* parser = static_cast<lxb_css_parser_t*>(_cssParser);
304
- auto* selectors = static_cast<lxb_selectors_t*>(_selectors);
305
- auto* doc = static_cast<lxb_html_document_t*>(_document);
434
+ // ── Doctype ──────────────────────────────────────────────────────────────────
306
435
 
307
- lxb_css_selector_list_t* list = lxb_css_selectors_parse(parser,
308
- reinterpret_cast<const lxb_char_t*>(selector.data()), selector.size());
309
- std::vector<std::string> results;
310
- if (list) {
311
- lxb_selectors_find(selectors, lxb_dom_interface_node(doc), list, findAllStringCallback, &results);
312
- lxb_css_selector_list_destroy_memory(list);
313
- }
314
- return results;
436
+ void* LexborDocument::doctype() const {
437
+ if (!_document) return nullptr;
438
+ auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
439
+ return dom_doc->doctype;
315
440
  }
316
441
 
317
- std::optional<std::string> LexborDocument::getAttribute(const std::string& selector, const std::string& attr) const {
318
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
319
- if (!el) return std::nullopt;
320
- size_t val_len;
321
- const lxb_char_t* val = lxb_dom_element_get_attribute(el,
322
- reinterpret_cast<const lxb_char_t*>(attr.data()), attr.size(), &val_len);
323
- if (!val) return std::nullopt;
324
- return std::string(reinterpret_cast<const char*>(val), val_len);
325
- }
326
-
327
- void LexborDocument::setAttribute(const std::string& selector, const std::string& attr, const std::string& value) {
328
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
329
- if (!el) return;
330
- lxb_dom_element_set_attribute(el,
331
- reinterpret_cast<const lxb_char_t*>(attr.data()), attr.size(),
332
- reinterpret_cast<const lxb_char_t*>(value.data()), value.size());
333
- }
334
-
335
- std::optional<std::string> LexborDocument::getTextContent(const std::string& selector) const {
336
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
337
- if (!el) return std::nullopt;
338
- lxb_dom_node_t* node = lxb_dom_interface_node(el);
339
- size_t len;
340
- lxb_char_t* text = lxb_dom_node_text_content(node, &len);
341
- if (!text) return std::nullopt;
342
- std::string result(reinterpret_cast<char*>(text), len);
343
- lxb_dom_document_destroy_text(node->owner_document, text);
344
- return result;
442
+ std::string LexborDocument::doctypeName(void* voidDoctype) const {
443
+ if (!voidDoctype) return "";
444
+ size_t len = 0;
445
+ const lxb_char_t* name = lxb_dom_document_type_name(
446
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
447
+ return name ? std::string(reinterpret_cast<const char*>(name), len) : "";
345
448
  }
346
449
 
347
- std::optional<std::string> LexborDocument::getInnerHTML(const std::string& selector) const {
348
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
349
- if (!el) return std::nullopt;
350
- std::string result;
351
- lxb_dom_node_t* child = lxb_dom_interface_node(el)->first_child;
450
+ std::string LexborDocument::doctypePublicId(void* voidDoctype) const {
451
+ if (!voidDoctype) return "";
452
+ size_t len = 0;
453
+ const lxb_char_t* id = lxb_dom_document_type_public_id(
454
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
455
+ return id ? std::string(reinterpret_cast<const char*>(id), len) : "";
456
+ }
457
+
458
+ std::string LexborDocument::doctypeSystemId(void* voidDoctype) const {
459
+ if (!voidDoctype) return "";
460
+ size_t len = 0;
461
+ const lxb_char_t* id = lxb_dom_document_type_system_id(
462
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
463
+ return id ? std::string(reinterpret_cast<const char*>(id), len) : "";
464
+ }
465
+
466
+ // ── Node.normalize() ─────────────────────────────────────────────────────────
467
+
468
+ namespace {
469
+
470
+ void normalize_children(lxb_dom_node_t* parent) {
471
+ lxb_dom_node_t* child = parent->first_child;
352
472
  while (child) {
353
- result += serializeNode(child);
354
- child = child->next;
473
+ lxb_dom_node_t* next = child->next;
474
+
475
+ if (child->type == LXB_DOM_NODE_TYPE_TEXT) {
476
+ auto* cd = reinterpret_cast<lxb_dom_character_data_t*>(child);
477
+ std::string merged(reinterpret_cast<const char*>(cd->data.data), cd->data.length);
478
+ size_t old_len = cd->data.length;
479
+
480
+ lxb_dom_node_t* sibling = next;
481
+ while (sibling && sibling->type == LXB_DOM_NODE_TYPE_TEXT) {
482
+ auto* scd = reinterpret_cast<lxb_dom_character_data_t*>(sibling);
483
+ merged.append(reinterpret_cast<const char*>(scd->data.data), scd->data.length);
484
+ lxb_dom_node_t* dead = sibling;
485
+ sibling = sibling->next;
486
+ lxb_dom_node_remove(dead);
487
+ lxb_dom_node_destroy(dead);
488
+ }
489
+
490
+ if (merged.empty()) {
491
+ lxb_dom_node_remove(child);
492
+ lxb_dom_node_destroy(child);
493
+ } else {
494
+ lxb_dom_character_data_replace(cd,
495
+ reinterpret_cast<const lxb_char_t*>(merged.data()), merged.size(),
496
+ 0, old_len);
497
+ }
498
+ next = sibling;
499
+ } else if (child->first_child) {
500
+ normalize_children(child);
501
+ }
502
+
503
+ child = next;
355
504
  }
356
- return result;
357
505
  }
358
506
 
359
- void LexborDocument::setInnerHTML(const std::string& selector, const std::string& html) {
360
- auto* el = static_cast<lxb_dom_element_t*>(findFirst(selector));
361
- if (!el) return;
362
- setInnerHTMLOnEl(el, html);
507
+ } // namespace
508
+
509
+ void LexborDocument::normalize(void* voidNode) {
510
+ auto* node = static_cast<lxb_dom_node_t*>(voidNode);
511
+ if (!node) return;
512
+ normalize_children(node);
513
+ }
514
+
515
+ // ── Node.compareDocumentPosition() ──────────────────────────────────────────
516
+
517
+ namespace {
518
+
519
+ constexpr int kDocumentPositionDisconnected = 0x01;
520
+ constexpr int kDocumentPositionPreceding = 0x02;
521
+ constexpr int kDocumentPositionFollowing = 0x04;
522
+ constexpr int kDocumentPositionContains = 0x08;
523
+ constexpr int kDocumentPositionContainedBy = 0x10;
524
+ constexpr int kDocumentPositionImplementationSpecific = 0x20;
525
+
526
+ lxb_dom_node_t* root_of(lxb_dom_node_t* n) {
527
+ while (n->parent) n = n->parent;
528
+ return n;
529
+ }
530
+
531
+ bool is_ancestor(lxb_dom_node_t* ancestor, lxb_dom_node_t* node) {
532
+ for (auto* p = node->parent; p; p = p->parent) {
533
+ if (p == ancestor) return true;
534
+ }
535
+ return false;
536
+ }
537
+
538
+ } // namespace
539
+
540
+ int LexborDocument::compareDocumentPosition(void* voidA, void* voidB) const {
541
+ auto* a = static_cast<lxb_dom_node_t*>(voidA);
542
+ auto* b = static_cast<lxb_dom_node_t*>(voidB);
543
+ if (!a || !b || a == b) return 0;
544
+
545
+ if (root_of(a) != root_of(b)) {
546
+ return kDocumentPositionDisconnected | kDocumentPositionImplementationSpecific
547
+ | kDocumentPositionFollowing;
548
+ }
549
+
550
+ // b is a descendant of a
551
+ if (is_ancestor(a, b)) return kDocumentPositionContainedBy | kDocumentPositionFollowing;
552
+ // a is a descendant of b
553
+ if (is_ancestor(b, a)) return kDocumentPositionContains | kDocumentPositionPreceding;
554
+
555
+ std::vector<lxb_dom_node_t*> chainA, chainB;
556
+ for (auto* p = a; p; p = p->parent) chainA.push_back(p);
557
+ for (auto* p = b; p; p = p->parent) chainB.push_back(p);
558
+ std::reverse(chainA.begin(), chainA.end());
559
+ std::reverse(chainB.begin(), chainB.end());
560
+
561
+ size_t i = 0;
562
+ while (i < chainA.size() && i < chainB.size() && chainA[i] == chainB[i]) i++;
563
+ // chainA[i-1] == chainB[i-1] is the lowest common ancestor (guaranteed to
564
+ // exist since both nodes share the same root and neither contains the other).
565
+ lxb_dom_node_t* lca = chainA[i - 1];
566
+ lxb_dom_node_t* branchA = chainA[i];
567
+ lxb_dom_node_t* branchB = chainB[i];
568
+
569
+ for (lxb_dom_node_t* c = lca->first_child; c; c = c->next) {
570
+ if (c == branchA) return kDocumentPositionFollowing;
571
+ if (c == branchB) return kDocumentPositionPreceding;
572
+ }
573
+ return kDocumentPositionDisconnected | kDocumentPositionImplementationSpecific
574
+ | kDocumentPositionFollowing;
363
575
  }
364
576
 
365
577
  } // namespace margelo::nitro::nitrojsdom
@@ -1,7 +1,6 @@
1
1
  #pragma once
2
2
 
3
3
  #include <string>
4
- #include <optional>
5
4
  #include <vector>
6
5
 
7
6
  namespace margelo::nitro::nitrojsdom {
@@ -39,9 +38,52 @@ public:
39
38
  void* createComment(const std::string& text);
40
39
  void* createDocumentFragment();
41
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
+
55
+ // ── Shadow DOM ─────────────────────────────────────────────────────────────
56
+ // mode: 0 = open, 1 = closed (matches lxb_dom_shadow_root_mode_t).
57
+ void* createShadowRoot(void* hostElement, int mode);
58
+
59
+ // ── <template> ─────────────────────────────────────────────────────────────
60
+ // Returns the lxb_dom_document_fragment_t* backing a <template> element's
61
+ // `content`. Lexbor allocates this at element-creation time (both for parsed
62
+ // and createElement()'d templates), so this is a plain field read.
63
+ void* templateContent(void* templateEl) const;
64
+
65
+ // ── Doctype ────────────────────────────────────────────────────────────────
66
+ // Returns the lxb_dom_document_type_t* for this document, or nullptr if the
67
+ // parsed HTML had no doctype.
68
+ void* doctype() const;
69
+ std::string doctypeName(void* doctype) const;
70
+ std::string doctypePublicId(void* doctype) const;
71
+ std::string doctypeSystemId(void* doctype) const;
72
+
73
+ // ── Node.normalize() ───────────────────────────────────────────────────────
74
+ // Merges adjacent Text node siblings and removes empty ones, recursively
75
+ // through node's subtree (node itself is not replaced, only its descendants).
76
+ void normalize(void* node);
77
+
78
+ // ── Node.compareDocumentPosition() ────────────────────────────────────────
79
+ // Returns the DOM DOCUMENT_POSITION_* bitmask describing nodeB's position
80
+ // relative to nodeA (mirrors `nodeA.compareDocumentPosition(nodeB)`).
81
+ int compareDocumentPosition(void* nodeA, void* nodeB) const;
82
+
42
83
  // ── Element content ───────────────────────────────────────────────────────
43
84
  void setTextContentOnEl(void* element, const std::string& text);
44
85
  void setInnerHTMLOnEl(void* element, const std::string& html);
86
+ void setInnerHTMLOnShadowRoot(void* shadowRoot, void* hostElement, const std::string& html);
45
87
 
46
88
  // Like setInnerHTMLOnEl, but returns the detached parsed nodes instead of
47
89
  // replacing contextElement's children — used by insertAdjacentHTML.
@@ -50,15 +92,6 @@ public:
50
92
  // ── Selector matching ─────────────────────────────────────────────────────
51
93
  bool matchesSelector(void* element, const std::string& sel) const;
52
94
 
53
- // ── Legacy selector-based API (kept for backward compat) ─────────────────
54
- std::optional<std::string> querySelector(const std::string& selector) const;
55
- std::vector<std::string> querySelectorAll(const std::string& selector) const;
56
- std::optional<std::string> getAttribute(const std::string& selector, const std::string& attr) const;
57
- void setAttribute(const std::string& selector, const std::string& attr, const std::string& value);
58
- std::optional<std::string> getTextContent(const std::string& selector) const;
59
- std::optional<std::string> getInnerHTML(const std::string& selector) const;
60
- void setInnerHTML(const std::string& selector, const std::string& html);
61
-
62
95
  private:
63
96
  void* _document { nullptr };
64
97
  void* _cssParser { nullptr };
@@ -4,12 +4,30 @@
4
4
  #include "Storage.hpp"
5
5
  #include "QuickJSRuntime.hpp"
6
6
  #include "../lexbor/LexborDocument.hpp"
7
+ #include "bindings/DOMExceptionBindings.hpp"
7
8
  #include "bindings/ElementBindings.hpp"
8
9
  #include "bindings/DocumentBindings.hpp"
10
+ #include "bindings/DOMParserBindings.hpp"
9
11
  #include "bindings/EventBindings.hpp"
10
12
  #include "bindings/TimerBindings.hpp"
11
13
  #include "bindings/WindowBindings.hpp"
12
14
  #include "bindings/FetchBindings.hpp"
15
+ #include "bindings/LiveCollectionBindings.hpp"
16
+ #include "bindings/UrlBindings.hpp"
17
+ #include "bindings/AbortBindings.hpp"
18
+ #include "bindings/TextEncodingBindings.hpp"
19
+ #include "bindings/FormBindings.hpp"
20
+ #include "bindings/BlobBindings.hpp"
21
+ #include "bindings/CSSOMBindings.hpp"
22
+ #include "bindings/ShadowRootBindings.hpp"
23
+ #include "bindings/CustomElementsBindings.hpp"
24
+ #include "bindings/CookieBindings.hpp"
25
+ #include "bindings/TemplateBindings.hpp"
26
+ #include "bindings/SlotBindings.hpp"
27
+ #include "bindings/XmlSerializerBindings.hpp"
28
+ #include "bindings/LayoutStubBindings.hpp"
29
+ #include "bindings/TreeWalkerBindings.hpp"
30
+ #include "bindings/EventTargetBindings.hpp"
13
31
  #include <lexbor/html/html.h>
14
32
  #include <lexbor/dom/dom.h>
15
33
 
@@ -22,14 +40,32 @@ namespace margelo::nitro::nitrojsdom {
22
40
 
23
41
  void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
24
42
  JSContext* ctx = static_cast<JSContext*>(runtime->context());
25
- (void)document;
43
+ register_document(ctx, document); // so doc_for_node() resolves nodes back to the primary document
26
44
 
45
+ DOMExceptionBindings::install(ctx); // no dependencies; other modules throw DOMException instances
46
+ LiveCollectionBindings::install(ctx);
27
47
  ElementBindings::install(ctx); // registers the Element class + proto
48
+ ShadowRootBindings::install(ctx); // needs Element's proto + js_node_class_id's proto
28
49
  DocumentBindings::install(ctx); // creates globalThis.document
50
+ DOMParserBindings::install(ctx); // adds document.implementation + globalThis.DOMParser; needs globalThis.document
51
+ CookieBindings::install(ctx); // needs globalThis.document to exist
52
+ TemplateBindings::install(ctx); // needs Element's proto
53
+ CustomElementsBindings::install(ctx); // needs Element/ShadowRoot protos + globalThis.document
54
+ CSSOMBindings::install(ctx); // needs Element's proto + globalThis.document to exist
29
55
  EventBindings::install(ctx); // needs Element's proto + globalThis.document to exist
30
56
  TimerBindings::install(ctx);
31
57
  WindowBindings::install(ctx);
58
+ UrlBindings::install(ctx);
59
+ AbortBindings::install(ctx);
60
+ TextEncodingBindings::install(ctx);
61
+ BlobBindings::install(ctx); // uses TextEncoder/TextDecoder + btoa, so must run after both
32
62
  FetchBindings::install(ctx); // XHR bootstrap uses `new Event(...)`, so must run after EventBindings
63
+ FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
64
+ SlotBindings::install(ctx); // uses Event/dispatchEvent + Element/ShadowRoot protos, so must run after EventBindings/ShadowRootBindings
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
33
69
 
34
70
  // ── localStorage / sessionStorage ──────────────────────────────────────────
35
71
  {