@salve-software/react-native-nitro-jsdom 1.0.1 → 1.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 (56) hide show
  1. package/android/CMakeLists.txt +14 -0
  2. package/cpp/HybridHtmlSandbox.cpp +16 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +217 -67
  5. package/cpp/lexbor/LexborDocument.hpp +29 -10
  6. package/cpp/quickjs/DOMBindings.cpp +28 -0
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -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/DocumentBindings.cpp +84 -4
  25. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  26. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  27. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  28. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  29. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  30. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  32. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  34. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  35. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  36. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  38. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  40. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  41. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  42. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  43. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  44. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  46. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  48. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  50. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  51. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  53. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  54. package/package.json +1 -1
  55. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  56. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -48,6 +48,7 @@ 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,6 +56,19 @@ add_library(${PACKAGE_NAME} SHARED
55
56
  ../cpp/quickjs/bindings/TimerBindings.cpp
56
57
  ../cpp/quickjs/bindings/WindowBindings.cpp
57
58
  ../cpp/quickjs/bindings/FetchBindings.cpp
59
+ ../cpp/quickjs/bindings/LiveCollectionBindings.cpp
60
+ ../cpp/quickjs/bindings/UrlBindings.cpp
61
+ ../cpp/quickjs/bindings/AbortBindings.cpp
62
+ ../cpp/quickjs/bindings/TextEncodingBindings.cpp
63
+ ../cpp/quickjs/bindings/FormBindings.cpp
64
+ ../cpp/quickjs/bindings/BlobBindings.cpp
65
+ ../cpp/quickjs/bindings/CSSOMBindings.cpp
66
+ ../cpp/quickjs/bindings/ShadowRootBindings.cpp
67
+ ../cpp/quickjs/bindings/CustomElementsBindings.cpp
68
+ ../cpp/quickjs/bindings/CookieBindings.cpp
69
+ ../cpp/quickjs/bindings/TemplateBindings.cpp
70
+ ../cpp/quickjs/bindings/SlotBindings.cpp
71
+ ../cpp/quickjs/bindings/XmlSerializerBindings.cpp
58
72
  ${QUICKJS_SOURCES}
59
73
  )
60
74
 
@@ -34,20 +34,33 @@ 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
+ try {
59
+ _runtime->evaluate(
60
+ "document.dispatchEvent(new Event('DOMContentLoaded', { bubbles: true, cancelable: true }));"
61
+ "document.dispatchEvent(new Event('load'));");
62
+ } catch (...) { }
63
+
51
64
  _initialized = true;
52
65
  }
53
66
 
@@ -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,17 @@
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>
6
9
  #include <stdexcept>
10
+ #include <cctype>
11
+ #include <cstring>
12
+ #include <unordered_set>
13
+ #include <vector>
14
+ #include <algorithm>
7
15
 
8
16
  namespace margelo::nitro::nitrojsdom {
9
17
 
@@ -22,6 +30,26 @@ static std::string serializeNode(lxb_dom_node_t* node) {
22
30
  return result;
23
31
  }
24
32
 
33
+ bool is_javascript_mime_type(const std::string& type) {
34
+ if (type.empty()) return true;
35
+ std::string t;
36
+ t.reserve(type.size());
37
+ for (char c : type) t += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
38
+ size_t start = t.find_first_not_of(" \t\n\r\f");
39
+ if (start == std::string::npos) return true;
40
+ size_t end = t.find_last_not_of(" \t\n\r\f");
41
+ t = t.substr(start, end - start + 1);
42
+
43
+ static const std::unordered_set<std::string> kJsTypes = {
44
+ "text/javascript", "application/javascript", "application/ecmascript",
45
+ "application/x-ecmascript", "application/x-javascript", "text/ecmascript",
46
+ "text/javascript1.0", "text/javascript1.1", "text/javascript1.2",
47
+ "text/javascript1.3", "text/javascript1.4", "text/javascript1.5",
48
+ "text/jscript", "text/livescript", "text/x-ecmascript", "text/x-javascript",
49
+ };
50
+ return kJsTypes.count(t) > 0;
51
+ }
52
+
25
53
  struct FindFirstCtx { lxb_dom_element_t* result = nullptr; };
26
54
 
27
55
  static lxb_status_t findFirstCallback(lxb_dom_node_t* node, lxb_css_selector_specificity_t, void* ctx) {
@@ -29,11 +57,6 @@ static lxb_status_t findFirstCallback(lxb_dom_node_t* node, lxb_css_selector_spe
29
57
  return LXB_STATUS_STOP;
30
58
  }
31
59
 
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
60
  static lxb_status_t findAllPtrCallback(lxb_dom_node_t* node, lxb_css_selector_specificity_t, void* ctx) {
38
61
  static_cast<std::vector<void*>*>(ctx)->push_back(lxb_dom_interface_element(node));
39
62
  return LXB_STATUS_OK;
@@ -134,14 +157,14 @@ std::vector<void*> LexborDocument::querySelectorAll_el(const std::string& sel) c
134
157
 
135
158
  void* LexborDocument::querySelectorFromEl(void* element, const std::string& sel) const {
136
159
  if (!element) return nullptr;
137
- return findFirstFrom(lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(element)), sel);
160
+ return findFirstFrom(static_cast<lxb_dom_node_t*>(element), sel);
138
161
  }
139
162
 
140
163
  std::vector<void*> LexborDocument::querySelectorAllFromEl(void* element, const std::string& sel) const {
141
164
  if (!_document || !element) return {};
142
165
  auto* parser = static_cast<lxb_css_parser_t*>(_cssParser);
143
166
  auto* selectors = static_cast<lxb_selectors_t*>(_selectors);
144
- auto* node = lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(element));
167
+ auto* node = static_cast<lxb_dom_node_t*>(element);
145
168
 
146
169
  lxb_css_selector_list_t* list = lxb_css_selectors_parse(parser,
147
170
  reinterpret_cast<const lxb_char_t*>(sel.data()), sel.size());
@@ -179,7 +202,15 @@ std::vector<std::string> LexborDocument::getScriptContents() const {
179
202
  contents.reserve(scriptEls.size());
180
203
 
181
204
  for (void* el : scriptEls) {
182
- auto* node = lxb_dom_interface_node(static_cast<lxb_dom_element_t*>(el));
205
+ auto* element = static_cast<lxb_dom_element_t*>(el);
206
+
207
+ size_t type_len = 0;
208
+ const lxb_char_t* type_attr = lxb_dom_element_get_attribute(element,
209
+ reinterpret_cast<const lxb_char_t*>("type"), 4, &type_len);
210
+ std::string type = type_attr ? std::string(reinterpret_cast<const char*>(type_attr), type_len) : "";
211
+ if (!is_javascript_mime_type(type)) continue;
212
+
213
+ auto* node = lxb_dom_interface_node(element);
183
214
  size_t len = 0;
184
215
  lxb_char_t* text = lxb_dom_node_text_content(node, &len);
185
216
  if (text && len > 0) {
@@ -219,6 +250,16 @@ void* LexborDocument::createDocumentFragment() {
219
250
  return lxb_dom_document_create_document_fragment(dom_doc);
220
251
  }
221
252
 
253
+ void* LexborDocument::createShadowRoot(void* hostElement, int mode) {
254
+ if (!_document || !hostElement) return nullptr;
255
+ auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
256
+ auto* shadow = lxb_dom_shadow_root_interface_create(dom_doc);
257
+ if (!shadow) return nullptr;
258
+ shadow->host = static_cast<lxb_dom_element_t*>(hostElement);
259
+ shadow->mode = static_cast<lxb_dom_shadow_root_mode_t>(mode);
260
+ return shadow;
261
+ }
262
+
222
263
  // ── Element content ──────────────────────────────────────────────────────────
223
264
 
224
265
  void LexborDocument::setTextContentOnEl(void* element, const std::string& text) {
@@ -242,10 +283,40 @@ void LexborDocument::setTextContentOnEl(void* element, const std::string& text)
242
283
  }
243
284
  }
244
285
 
286
+ void LexborDocument::setInnerHTMLOnShadowRoot(void* shadowRoot, void* hostElement, const std::string& html) {
287
+ auto* host = static_cast<lxb_dom_element_t*>(hostElement);
288
+ auto* doc = static_cast<lxb_html_document_t*>(_document);
289
+ auto* root = static_cast<lxb_dom_node_t*>(shadowRoot);
290
+
291
+ lxb_dom_node_t* frag = lxb_html_document_parse_fragment(doc, host,
292
+ reinterpret_cast<const lxb_char_t*>(html.data()), html.size());
293
+ if (!frag) return;
294
+
295
+ while (root->first_child) lxb_dom_node_destroy_deep(root->first_child);
296
+
297
+ while (frag->first_child) {
298
+ lxb_dom_node_t* child = frag->first_child;
299
+ lxb_dom_node_remove(child);
300
+ lxb_dom_node_insert_child(root, child);
301
+ }
302
+
303
+ lxb_dom_node_destroy(frag);
304
+ }
305
+
245
306
  void LexborDocument::setInnerHTMLOnEl(void* element, const std::string& html) {
246
307
  auto* el = static_cast<lxb_dom_element_t*>(element);
247
308
  auto* doc = static_cast<lxb_html_document_t*>(_document);
248
- auto* root = lxb_dom_interface_node(el);
309
+
310
+ // <template>.innerHTML populates .content (a separate inert fragment), not
311
+ // the template element's own light-DOM children — matches the parser's own
312
+ // "in template" insertion mode behavior for parsed <template> markup.
313
+ size_t tag_len = 0;
314
+ const lxb_char_t* tag_name = lxb_dom_element_local_name(el, &tag_len);
315
+ bool is_template = tag_len == 8 && memcmp(tag_name, "template", 8) == 0;
316
+
317
+ lxb_dom_node_t* root = is_template
318
+ ? lxb_dom_interface_node(reinterpret_cast<lxb_html_template_element_t*>(el)->content)
319
+ : lxb_dom_interface_node(el);
249
320
 
250
321
  lxb_dom_node_t* frag = lxb_html_document_parse_fragment(doc, el,
251
322
  reinterpret_cast<const lxb_char_t*>(html.data()), html.size());
@@ -290,76 +361,155 @@ bool LexborDocument::matchesSelector(void* element, const std::string& sel) cons
290
361
  return false;
291
362
  }
292
363
 
293
- // ── Legacy selector-based API ────────────────────────────────────────────────
364
+ // ── <template> ────────────────────────────────────────────────────────────
294
365
 
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));
366
+ void* LexborDocument::templateContent(void* templateEl) const {
367
+ if (!templateEl) return nullptr;
368
+ auto* tpl = reinterpret_cast<lxb_html_template_element_t*>(templateEl);
369
+ return tpl->content;
299
370
  }
300
371
 
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);
372
+ // ── Doctype ──────────────────────────────────────────────────────────────────
306
373
 
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;
374
+ void* LexborDocument::doctype() const {
375
+ if (!_document) return nullptr;
376
+ auto* dom_doc = lxb_dom_interface_document(static_cast<lxb_html_document_t*>(_document));
377
+ return dom_doc->doctype;
315
378
  }
316
379
 
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;
380
+ std::string LexborDocument::doctypeName(void* voidDoctype) const {
381
+ if (!voidDoctype) return "";
382
+ size_t len = 0;
383
+ const lxb_char_t* name = lxb_dom_document_type_name(
384
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
385
+ return name ? std::string(reinterpret_cast<const char*>(name), len) : "";
345
386
  }
346
387
 
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;
388
+ std::string LexborDocument::doctypePublicId(void* voidDoctype) const {
389
+ if (!voidDoctype) return "";
390
+ size_t len = 0;
391
+ const lxb_char_t* id = lxb_dom_document_type_public_id(
392
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
393
+ return id ? std::string(reinterpret_cast<const char*>(id), len) : "";
394
+ }
395
+
396
+ std::string LexborDocument::doctypeSystemId(void* voidDoctype) const {
397
+ if (!voidDoctype) return "";
398
+ size_t len = 0;
399
+ const lxb_char_t* id = lxb_dom_document_type_system_id(
400
+ static_cast<lxb_dom_document_type_t*>(voidDoctype), &len);
401
+ return id ? std::string(reinterpret_cast<const char*>(id), len) : "";
402
+ }
403
+
404
+ // ── Node.normalize() ─────────────────────────────────────────────────────────
405
+
406
+ namespace {
407
+
408
+ void normalize_children(lxb_dom_node_t* parent) {
409
+ lxb_dom_node_t* child = parent->first_child;
352
410
  while (child) {
353
- result += serializeNode(child);
354
- child = child->next;
411
+ lxb_dom_node_t* next = child->next;
412
+
413
+ if (child->type == LXB_DOM_NODE_TYPE_TEXT) {
414
+ auto* cd = reinterpret_cast<lxb_dom_character_data_t*>(child);
415
+ std::string merged(reinterpret_cast<const char*>(cd->data.data), cd->data.length);
416
+ size_t old_len = cd->data.length;
417
+
418
+ lxb_dom_node_t* sibling = next;
419
+ while (sibling && sibling->type == LXB_DOM_NODE_TYPE_TEXT) {
420
+ auto* scd = reinterpret_cast<lxb_dom_character_data_t*>(sibling);
421
+ merged.append(reinterpret_cast<const char*>(scd->data.data), scd->data.length);
422
+ lxb_dom_node_t* dead = sibling;
423
+ sibling = sibling->next;
424
+ lxb_dom_node_remove(dead);
425
+ lxb_dom_node_destroy(dead);
426
+ }
427
+
428
+ if (merged.empty()) {
429
+ lxb_dom_node_remove(child);
430
+ lxb_dom_node_destroy(child);
431
+ } else {
432
+ lxb_dom_character_data_replace(cd,
433
+ reinterpret_cast<const lxb_char_t*>(merged.data()), merged.size(),
434
+ 0, old_len);
435
+ }
436
+ next = sibling;
437
+ } else if (child->first_child) {
438
+ normalize_children(child);
439
+ }
440
+
441
+ child = next;
355
442
  }
356
- return result;
357
443
  }
358
444
 
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);
445
+ } // namespace
446
+
447
+ void LexborDocument::normalize(void* voidNode) {
448
+ auto* node = static_cast<lxb_dom_node_t*>(voidNode);
449
+ if (!node) return;
450
+ normalize_children(node);
451
+ }
452
+
453
+ // ── Node.compareDocumentPosition() ──────────────────────────────────────────
454
+
455
+ namespace {
456
+
457
+ constexpr int kDocumentPositionDisconnected = 0x01;
458
+ constexpr int kDocumentPositionPreceding = 0x02;
459
+ constexpr int kDocumentPositionFollowing = 0x04;
460
+ constexpr int kDocumentPositionContains = 0x08;
461
+ constexpr int kDocumentPositionContainedBy = 0x10;
462
+ constexpr int kDocumentPositionImplementationSpecific = 0x20;
463
+
464
+ lxb_dom_node_t* root_of(lxb_dom_node_t* n) {
465
+ while (n->parent) n = n->parent;
466
+ return n;
467
+ }
468
+
469
+ bool is_ancestor(lxb_dom_node_t* ancestor, lxb_dom_node_t* node) {
470
+ for (auto* p = node->parent; p; p = p->parent) {
471
+ if (p == ancestor) return true;
472
+ }
473
+ return false;
474
+ }
475
+
476
+ } // namespace
477
+
478
+ int LexborDocument::compareDocumentPosition(void* voidA, void* voidB) const {
479
+ auto* a = static_cast<lxb_dom_node_t*>(voidA);
480
+ auto* b = static_cast<lxb_dom_node_t*>(voidB);
481
+ if (!a || !b || a == b) return 0;
482
+
483
+ if (root_of(a) != root_of(b)) {
484
+ return kDocumentPositionDisconnected | kDocumentPositionImplementationSpecific
485
+ | kDocumentPositionFollowing;
486
+ }
487
+
488
+ // b is a descendant of a
489
+ if (is_ancestor(a, b)) return kDocumentPositionContainedBy | kDocumentPositionFollowing;
490
+ // a is a descendant of b
491
+ if (is_ancestor(b, a)) return kDocumentPositionContains | kDocumentPositionPreceding;
492
+
493
+ std::vector<lxb_dom_node_t*> chainA, chainB;
494
+ for (auto* p = a; p; p = p->parent) chainA.push_back(p);
495
+ for (auto* p = b; p; p = p->parent) chainB.push_back(p);
496
+ std::reverse(chainA.begin(), chainA.end());
497
+ std::reverse(chainB.begin(), chainB.end());
498
+
499
+ size_t i = 0;
500
+ while (i < chainA.size() && i < chainB.size() && chainA[i] == chainB[i]) i++;
501
+ // chainA[i-1] == chainB[i-1] is the lowest common ancestor (guaranteed to
502
+ // exist since both nodes share the same root and neither contains the other).
503
+ lxb_dom_node_t* lca = chainA[i - 1];
504
+ lxb_dom_node_t* branchA = chainA[i];
505
+ lxb_dom_node_t* branchB = chainB[i];
506
+
507
+ for (lxb_dom_node_t* c = lca->first_child; c; c = c->next) {
508
+ if (c == branchA) return kDocumentPositionFollowing;
509
+ if (c == branchB) return kDocumentPositionPreceding;
510
+ }
511
+ return kDocumentPositionDisconnected | kDocumentPositionImplementationSpecific
512
+ | kDocumentPositionFollowing;
363
513
  }
364
514
 
365
515
  } // 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,38 @@ public:
39
38
  void* createComment(const std::string& text);
40
39
  void* createDocumentFragment();
41
40
 
41
+ // ── Shadow DOM ─────────────────────────────────────────────────────────────
42
+ // mode: 0 = open, 1 = closed (matches lxb_dom_shadow_root_mode_t).
43
+ void* createShadowRoot(void* hostElement, int mode);
44
+
45
+ // ── <template> ─────────────────────────────────────────────────────────────
46
+ // Returns the lxb_dom_document_fragment_t* backing a <template> element's
47
+ // `content`. Lexbor allocates this at element-creation time (both for parsed
48
+ // and createElement()'d templates), so this is a plain field read.
49
+ void* templateContent(void* templateEl) const;
50
+
51
+ // ── Doctype ────────────────────────────────────────────────────────────────
52
+ // Returns the lxb_dom_document_type_t* for this document, or nullptr if the
53
+ // parsed HTML had no doctype.
54
+ void* doctype() const;
55
+ std::string doctypeName(void* doctype) const;
56
+ std::string doctypePublicId(void* doctype) const;
57
+ std::string doctypeSystemId(void* doctype) const;
58
+
59
+ // ── Node.normalize() ───────────────────────────────────────────────────────
60
+ // Merges adjacent Text node siblings and removes empty ones, recursively
61
+ // through node's subtree (node itself is not replaced, only its descendants).
62
+ void normalize(void* node);
63
+
64
+ // ── Node.compareDocumentPosition() ────────────────────────────────────────
65
+ // Returns the DOM DOCUMENT_POSITION_* bitmask describing nodeB's position
66
+ // relative to nodeA (mirrors `nodeA.compareDocumentPosition(nodeB)`).
67
+ int compareDocumentPosition(void* nodeA, void* nodeB) const;
68
+
42
69
  // ── Element content ───────────────────────────────────────────────────────
43
70
  void setTextContentOnEl(void* element, const std::string& text);
44
71
  void setInnerHTMLOnEl(void* element, const std::string& html);
72
+ void setInnerHTMLOnShadowRoot(void* shadowRoot, void* hostElement, const std::string& html);
45
73
 
46
74
  // Like setInnerHTMLOnEl, but returns the detached parsed nodes instead of
47
75
  // replacing contextElement's children — used by insertAdjacentHTML.
@@ -50,15 +78,6 @@ public:
50
78
  // ── Selector matching ─────────────────────────────────────────────────────
51
79
  bool matchesSelector(void* element, const std::string& sel) const;
52
80
 
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
81
  private:
63
82
  void* _document { nullptr };
64
83
  void* _cssParser { nullptr };
@@ -4,12 +4,26 @@
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"
9
10
  #include "bindings/EventBindings.hpp"
10
11
  #include "bindings/TimerBindings.hpp"
11
12
  #include "bindings/WindowBindings.hpp"
12
13
  #include "bindings/FetchBindings.hpp"
14
+ #include "bindings/LiveCollectionBindings.hpp"
15
+ #include "bindings/UrlBindings.hpp"
16
+ #include "bindings/AbortBindings.hpp"
17
+ #include "bindings/TextEncodingBindings.hpp"
18
+ #include "bindings/FormBindings.hpp"
19
+ #include "bindings/BlobBindings.hpp"
20
+ #include "bindings/CSSOMBindings.hpp"
21
+ #include "bindings/ShadowRootBindings.hpp"
22
+ #include "bindings/CustomElementsBindings.hpp"
23
+ #include "bindings/CookieBindings.hpp"
24
+ #include "bindings/TemplateBindings.hpp"
25
+ #include "bindings/SlotBindings.hpp"
26
+ #include "bindings/XmlSerializerBindings.hpp"
13
27
  #include <lexbor/html/html.h>
14
28
  #include <lexbor/dom/dom.h>
15
29
 
@@ -24,12 +38,26 @@ void DOMBindings::install(QuickJSRuntime* runtime, LexborDocument* document) {
24
38
  JSContext* ctx = static_cast<JSContext*>(runtime->context());
25
39
  (void)document;
26
40
 
41
+ DOMExceptionBindings::install(ctx); // no dependencies; other modules throw DOMException instances
42
+ LiveCollectionBindings::install(ctx);
27
43
  ElementBindings::install(ctx); // registers the Element class + proto
44
+ ShadowRootBindings::install(ctx); // needs Element's proto + js_node_class_id's proto
28
45
  DocumentBindings::install(ctx); // creates globalThis.document
46
+ CookieBindings::install(ctx); // needs globalThis.document to exist
47
+ TemplateBindings::install(ctx); // needs Element's proto
48
+ CustomElementsBindings::install(ctx); // needs Element/ShadowRoot protos + globalThis.document
49
+ CSSOMBindings::install(ctx); // needs Element's proto + globalThis.document to exist
29
50
  EventBindings::install(ctx); // needs Element's proto + globalThis.document to exist
30
51
  TimerBindings::install(ctx);
31
52
  WindowBindings::install(ctx);
53
+ UrlBindings::install(ctx);
54
+ AbortBindings::install(ctx);
55
+ TextEncodingBindings::install(ctx);
56
+ BlobBindings::install(ctx); // uses TextEncoder/TextDecoder + btoa, so must run after both
32
57
  FetchBindings::install(ctx); // XHR bootstrap uses `new Event(...)`, so must run after EventBindings
58
+ FormBindings::install(ctx); // uses globalThis.Element + globalThis.Event, so must run after both
59
+ SlotBindings::install(ctx); // uses Event/dispatchEvent + Element/ShadowRoot protos, so must run after EventBindings/ShadowRootBindings
60
+ XmlSerializerBindings::install(ctx); // pure serialization, no ordering requirement beyond ElementBindings
33
61
 
34
62
  // ── localStorage / sessionStorage ──────────────────────────────────────────
35
63
  {
@@ -8,8 +8,9 @@
8
8
 
9
9
  namespace margelo::nitro::nitrojsdom {
10
10
 
11
- JSClassID js_element_class_id = 0;
12
- JSClassID js_node_class_id = 0;
11
+ JSClassID js_element_class_id = 0;
12
+ JSClassID js_node_class_id = 0;
13
+ JSClassID js_shadow_root_class_id = 0;
13
14
 
14
15
  namespace {
15
16
  JSClassDef js_element_class = { "Element", .finalizer = nullptr };
@@ -59,6 +60,22 @@ void invalidate_node_cache_batch(JSContext* ctx, RuntimeContext* rctx, const std
59
60
  for (void* n : nodes) invalidate_node_cache(ctx, rctx, n);
60
61
  }
61
62
 
63
+ namespace {
64
+ void invalidate_node_cache_deep(JSContext* ctx, RuntimeContext* rctx, void* node_ptr) {
65
+ if (!node_ptr) return;
66
+ invalidate_node_cache(ctx, rctx, node_ptr);
67
+ for (lxb_dom_node_t* child = static_cast<lxb_dom_node_t*>(node_ptr)->first_child;
68
+ child; child = child->next) {
69
+ invalidate_node_cache_deep(ctx, rctx, child);
70
+ }
71
+ }
72
+ } // namespace
73
+
74
+ void invalidate_node_cache_deep_batch(JSContext* ctx, RuntimeContext* rctx, const std::vector<void*>& roots) {
75
+ if (!rctx) return;
76
+ for (void* root : roots) invalidate_node_cache_deep(ctx, rctx, root);
77
+ }
78
+
62
79
  void clear_node_cache(JSContext* ctx, RuntimeContext* rctx) {
63
80
  if (!rctx) return;
64
81
  for (auto& kv : rctx->node_wrapper_cache) {
@@ -85,6 +102,8 @@ lxb_dom_node_t* unwrap_node(JSContext* ctx, JSValue val) {
85
102
  void* p = JS_GetOpaque(val, js_element_class_id);
86
103
  if (p) return static_cast<lxb_dom_node_t*>(p);
87
104
  p = JS_GetOpaque(val, js_node_class_id);
105
+ if (p) return static_cast<lxb_dom_node_t*>(p);
106
+ p = JS_GetOpaque(val, js_shadow_root_class_id);
88
107
  return static_cast<lxb_dom_node_t*>(p);
89
108
  }
90
109
 
@@ -117,6 +136,19 @@ void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter,
117
136
  JS_FreeAtom(ctx, atom);
118
137
  }
119
138
 
139
+ JSValue js_illegal_constructor(JSContext* ctx, JSValue, int, JSValue*) {
140
+ return JS_ThrowTypeError(ctx, "Illegal constructor");
141
+ }
142
+
143
+ JSValue define_global_constructor(JSContext* ctx, const char* name, JSValue proto) {
144
+ JSValue ctor = JS_NewCFunction2(ctx, js_illegal_constructor, name, 0, JS_CFUNC_constructor, 0);
145
+ JS_SetConstructor(ctx, ctor, proto);
146
+ JSValue global = JS_GetGlobalObject(ctx);
147
+ JS_SetPropertyStr(ctx, global, name, JS_DupValue(ctx, ctor));
148
+ JS_FreeValue(ctx, global);
149
+ return ctor;
150
+ }
151
+
120
152
  bool get_bool_prop(JSContext* ctx, JSValue obj, const char* name) {
121
153
  JSValue v = JS_GetPropertyStr(ctx, obj, name);
122
154
  bool b = JS_ToBool(ctx, v) > 0;
@@ -34,11 +34,16 @@ class LexborDocument;
34
34
  // node class (Node traversal only). This prevents non-element nodes from
35
35
  // reaching lxb_dom_element_* calls.
36
36
  //
37
+ // js_shadow_root_class_id wraps lxb_dom_shadow_root_t* (see ShadowRootBindings).
38
+ //
37
39
  // unwrap_element() returns non-null only for element-class objects.
38
- // unwrap_node() returns non-null for either class (shared Node methods).
40
+ // unwrap_node() returns non-null for any of the three (shared Node methods
41
+ // like appendChild/childNodes work the same way on a
42
+ // ShadowRoot, since it embeds a document fragment's layout).
39
43
 
40
44
  extern JSClassID js_element_class_id;
41
45
  extern JSClassID js_node_class_id;
46
+ extern JSClassID js_shadow_root_class_id;
42
47
 
43
48
  JSValue make_element(JSContext* ctx, void* node_or_el);
44
49
  JSValue make_element_array(JSContext* ctx, const std::vector<void*>& elements);
@@ -57,6 +62,12 @@ void invalidate_node_cache(JSContext* ctx, RuntimeContext* rctx, void* node);
57
62
  void invalidate_node_cache_batch(JSContext* ctx, RuntimeContext* rctx, const std::vector<void*>& nodes);
58
63
  void clear_node_cache(JSContext* ctx, RuntimeContext* rctx);
59
64
 
65
+ // Same as invalidate_node_cache_batch, but also invalidates every descendant
66
+ // of each given node. Use before destroying a subtree with
67
+ // lxb_dom_node_destroy_deep(): that frees descendants too, and a JS wrapper
68
+ // left pointing at a freed descendant is a use-after-free waiting to happen.
69
+ void invalidate_node_cache_deep_batch(JSContext* ctx, RuntimeContext* rctx, const std::vector<void*>& roots);
70
+
60
71
  // ── Accessor property helper ──────────────────────────────────────────────────
61
72
 
62
73
  using GetterFn = JSValue (*)(JSContext*, JSValue);
@@ -64,6 +75,11 @@ using SetterFn = JSValue (*)(JSContext*, JSValue, JSValue);
64
75
 
65
76
  void define_prop(JSContext* ctx, JSValue obj, const char* name, GetterFn getter, SetterFn setter = nullptr);
66
77
 
78
+ // ── Global constructor helper (instanceof support) ────────────────────────────
79
+
80
+ JSValue js_illegal_constructor(JSContext* ctx, JSValue this_val, int argc, JSValue* argv);
81
+ JSValue define_global_constructor(JSContext* ctx, const char* name, JSValue proto);
82
+
67
83
  // ── Misc shared helpers ───────────────────────────────────────────────────────
68
84
 
69
85
  // Reads a boolean-ish property off any JS object (used by Event dispatch to read