@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
@@ -2,6 +2,8 @@
2
2
  #include "ClassListBindings.hpp"
3
3
  #include "DatasetBindings.hpp"
4
4
  #include "StyleBindings.hpp"
5
+ #include "LiveCollectionBindings.hpp"
6
+ #include "DOMExceptionBindings.hpp"
5
7
  #include "../DOMBindingsInternal.hpp"
6
8
  #include "../QuickJSRuntime.hpp"
7
9
  #include "../MutationObservers.hpp"
@@ -9,6 +11,9 @@
9
11
  #include <lexbor/html/html.h>
10
12
  #include <lexbor/dom/dom.h>
11
13
  #include <lexbor/dom/interfaces/character_data.h>
14
+ #include <lexbor/dom/interfaces/attr.h>
15
+ #include <lexbor/ns/const.h>
16
+ #include <lexbor/ns/ns.h>
12
17
  #include <cctype>
13
18
  #include <cstring>
14
19
  #include <optional>
@@ -27,10 +32,18 @@ JSValue js_el_get_tagName(JSContext* ctx, JSValue this_val) {
27
32
  auto* el = unwrap_element(ctx, this_val);
28
33
  if (!el) return JS_NewString(ctx, "");
29
34
  size_t len = 0;
30
- const lxb_char_t* name = lxb_dom_element_local_name(el, &len);
35
+ // qualified_name (prefix:localName) when the element has a namespace
36
+ // prefix (createElementNS('...', 'prefix:name')), falling back to the
37
+ // plain local name otherwise.
38
+ const lxb_char_t* name = lxb_dom_element_qualified_name(el, &len);
31
39
  if (!name || len == 0) return JS_NewString(ctx, "");
32
40
  std::string r(reinterpret_cast<const char*>(name), len);
33
- for (auto& c : r) c = (char)std::toupper((unsigned char)c);
41
+ // Only HTML-namespace elements are uppercased (per spec); SVG/MathML/custom
42
+ // namespace elements keep their (lexbor-lowercased) local name as-is.
43
+ auto* node = reinterpret_cast<lxb_dom_node_t*>(el);
44
+ if (node->ns == LXB_NS_HTML) {
45
+ for (auto& c : r) c = (char)std::toupper((unsigned char)c);
46
+ }
34
47
  return JS_NewStringLen(ctx, r.c_str(), r.size());
35
48
  }
36
49
 
@@ -100,6 +113,196 @@ JSValue js_el_set_className(JSContext* ctx, JSValue this_val, JSValue val) {
100
113
  return JS_UNDEFINED;
101
114
  }
102
115
 
116
+ // ── Form element properties (value / checked) ────────────────────────────────
117
+ // Simplified attribute-reflection model, matching this file's existing id/
118
+ // className getters: no separate "dirty value" IDL flag like real browsers,
119
+ // just direct reflection so scripts that set then read these back (the
120
+ // common embedded-widget pattern) see consistent results.
121
+
122
+ JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val);
123
+
124
+ bool element_tag_is(lxb_dom_element_t* el, const char* tag) {
125
+ size_t len = 0;
126
+ const lxb_char_t* name = lxb_dom_element_local_name(el, &len);
127
+ if (!name || len != strlen(tag)) return false;
128
+ for (size_t i = 0; i < len; i++) {
129
+ if (std::tolower(name[i]) != std::tolower((unsigned char)tag[i])) return false;
130
+ }
131
+ return true;
132
+ }
133
+
134
+ // HTMLOptionElement's value: the "value" attribute if present, else the
135
+ // option's text content.
136
+ std::string option_effective_value(lxb_dom_element_t* option) {
137
+ size_t len = 0;
138
+ const lxb_char_t* val = lxb_dom_element_get_attribute(option,
139
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
140
+ if (val) return std::string(reinterpret_cast<const char*>(val), len);
141
+ lxb_dom_node_t* node = lxb_dom_interface_node(option);
142
+ lxb_char_t* text = lxb_dom_node_text_content(node, &len);
143
+ if (!text) return "";
144
+ std::string result(reinterpret_cast<char*>(text), len);
145
+ lxb_dom_document_destroy_text(node->owner_document, text);
146
+ return result;
147
+ }
148
+
149
+ JSValue js_select_get_value(JSContext* ctx, lxb_dom_element_t* select_el) {
150
+ auto options = get_doc(ctx)->querySelectorAllFromEl(select_el, "option");
151
+ void* chosen = nullptr;
152
+ for (void* opt : options) {
153
+ if (lxb_dom_element_has_attribute(static_cast<lxb_dom_element_t*>(opt),
154
+ reinterpret_cast<const lxb_char_t*>("selected"), 8)) { chosen = opt; break; }
155
+ }
156
+ if (!chosen && !options.empty()) chosen = options.front();
157
+ if (!chosen) return JS_NewString(ctx, "");
158
+ return JS_NewString(ctx, option_effective_value(static_cast<lxb_dom_element_t*>(chosen)).c_str());
159
+ }
160
+
161
+ void js_select_set_value(JSContext* ctx, lxb_dom_element_t* select_el, const std::string& value) {
162
+ auto options = get_doc(ctx)->querySelectorAllFromEl(select_el, "option");
163
+ auto* rctx = get_ctx(ctx);
164
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
165
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>("selected");
166
+
167
+ for (void* opt : options) {
168
+ auto* opt_el = static_cast<lxb_dom_element_t*>(opt);
169
+ bool should_select = option_effective_value(opt_el) == value;
170
+ bool has_selected = lxb_dom_element_has_attribute(opt_el, attr_name, 8);
171
+
172
+ std::optional<std::string> old_val;
173
+ if (has_obs && has_selected && rctx->mutation_observers->hasAttributeOldValueObserver()) {
174
+ size_t len = 0;
175
+ const lxb_char_t* v = lxb_dom_element_get_attribute(opt_el, attr_name, 8, &len);
176
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
177
+ }
178
+
179
+ bool changed = false;
180
+ if (should_select && !has_selected) {
181
+ lxb_dom_element_set_attribute(opt_el, attr_name, 8, reinterpret_cast<const lxb_char_t*>(""), 0);
182
+ changed = true;
183
+ } else if (!should_select && has_selected) {
184
+ lxb_dom_element_remove_attribute(opt_el, attr_name, 8);
185
+ changed = true;
186
+ }
187
+
188
+ if (changed && has_obs) {
189
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(opt_el), "selected", old_val);
190
+ }
191
+ }
192
+ }
193
+
194
+ JSValue js_el_get_value(JSContext* ctx, JSValue this_val) {
195
+ auto* el = unwrap_element(ctx, this_val);
196
+ if (!el) return JS_NewString(ctx, "");
197
+ if (element_tag_is(el, "select")) return js_select_get_value(ctx, el);
198
+ if (element_tag_is(el, "textarea")) return js_el_get_textContent(ctx, this_val);
199
+ size_t len = 0;
200
+ const lxb_char_t* val = lxb_dom_element_get_attribute(el,
201
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
202
+ if (val) return JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len);
203
+
204
+ // A checkbox/radio with no "value" attribute defaults its value to "on".
205
+ if (element_tag_is(el, "input")) {
206
+ size_t type_len = 0;
207
+ const lxb_char_t* type_val = lxb_dom_element_get_attribute(el,
208
+ reinterpret_cast<const lxb_char_t*>("type"), 4, &type_len);
209
+ std::string type_str = type_val ? std::string(reinterpret_cast<const char*>(type_val), type_len) : "";
210
+ for (auto& c : type_str) c = (char)std::tolower((unsigned char)c);
211
+ if (type_str == "checkbox" || type_str == "radio") return JS_NewString(ctx, "on");
212
+ }
213
+ return JS_NewString(ctx, "");
214
+ }
215
+
216
+ JSValue js_el_set_value(JSContext* ctx, JSValue this_val, JSValue val) {
217
+ auto* el = unwrap_element(ctx, this_val);
218
+ if (!el) return JS_UNDEFINED;
219
+ const char* str = JS_ToCString(ctx, val);
220
+ if (!str) return JS_UNDEFINED;
221
+
222
+ if (element_tag_is(el, "select")) {
223
+ js_select_set_value(ctx, el, str);
224
+ } else if (element_tag_is(el, "textarea")) {
225
+ auto* rctx = get_ctx(ctx);
226
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
227
+ lxb_dom_node_t* node = lxb_dom_interface_node(el);
228
+
229
+ std::vector<void*> old_children;
230
+ lxb_dom_node_t* child = node->first_child;
231
+ while (child) { old_children.push_back(child); child = child->next; }
232
+ if (!old_children.empty()) {
233
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
234
+ if (has_obs) rctx->mutation_observers->disconnectDetachedTargets(old_children);
235
+ }
236
+
237
+ doc_for_node(ctx, el)->setTextContentOnEl(el, str);
238
+
239
+ if (has_obs) {
240
+ std::vector<void*> new_children;
241
+ child = node->first_child;
242
+ while (child) { new_children.push_back(child); child = child->next; }
243
+ rctx->mutation_observers->notifyChildList(ctx, node, new_children, {}, nullptr, nullptr);
244
+ }
245
+ } else {
246
+ auto* rctx = get_ctx(ctx);
247
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
248
+ std::optional<std::string> old_val;
249
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver()) {
250
+ size_t len = 0;
251
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el,
252
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
253
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
254
+ }
255
+ lxb_dom_element_set_attribute(el,
256
+ reinterpret_cast<const lxb_char_t*>("value"), 5,
257
+ reinterpret_cast<const lxb_char_t*>(str), strlen(str));
258
+ if (has_obs) {
259
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "value", old_val);
260
+ }
261
+ }
262
+
263
+ JS_FreeCString(ctx, str);
264
+ return JS_UNDEFINED;
265
+ }
266
+
267
+ JSValue js_el_get_checked(JSContext* ctx, JSValue this_val) {
268
+ auto* el = unwrap_element(ctx, this_val);
269
+ if (!el) return JS_FALSE;
270
+ bool has = lxb_dom_element_has_attribute(el,
271
+ reinterpret_cast<const lxb_char_t*>("checked"), 7);
272
+ return JS_NewBool(ctx, has);
273
+ }
274
+
275
+ JSValue js_el_set_checked(JSContext* ctx, JSValue this_val, JSValue val) {
276
+ auto* el = unwrap_element(ctx, this_val);
277
+ if (!el) return JS_UNDEFINED;
278
+ bool checked = JS_ToBool(ctx, val) > 0;
279
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>("checked");
280
+ bool has = lxb_dom_element_has_attribute(el, attr_name, 7);
281
+
282
+ auto* rctx = get_ctx(ctx);
283
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
284
+ std::optional<std::string> old_val;
285
+ if (has_obs && has && rctx->mutation_observers->hasAttributeOldValueObserver()) {
286
+ size_t len = 0;
287
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, 7, &len);
288
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
289
+ }
290
+
291
+ if (checked && !has) {
292
+ lxb_dom_element_set_attribute(el, attr_name, 7, reinterpret_cast<const lxb_char_t*>(""), 0);
293
+ if (has_obs) {
294
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "checked", std::nullopt);
295
+ }
296
+ } else if (!checked && has) {
297
+ lxb_dom_element_remove_attribute(el, attr_name, 7);
298
+ if (has_obs) {
299
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "checked", old_val);
300
+ }
301
+ }
302
+
303
+ return JS_UNDEFINED;
304
+ }
305
+
103
306
  JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val) {
104
307
  lxb_dom_node_t* node = unwrap_node(ctx, this_val);
105
308
  if (!node) return JS_NewString(ctx, "");
@@ -144,11 +347,11 @@ JSValue js_el_set_textContent(JSContext* ctx, JSValue this_val, JSValue val) {
144
347
  lxb_dom_node_t* child = node->first_child;
145
348
  while (child) { old_children.push_back(child); child = child->next; }
146
349
  if (!old_children.empty()) {
147
- invalidate_node_cache_batch(ctx, rctx, old_children);
350
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
148
351
  if (has_observers) rctx->mutation_observers->disconnectDetachedTargets(old_children);
149
352
  }
150
353
  }
151
- get_doc(ctx)->setTextContentOnEl(el, str);
354
+ doc_for_node(ctx, node)->setTextContentOnEl(el, str);
152
355
  if (has_observers) {
153
356
  std::vector<void*> new_children;
154
357
  lxb_dom_node_t* child = node->first_child;
@@ -189,11 +392,11 @@ JSValue js_el_set_innerHTML(JSContext* ctx, JSValue this_val, JSValue val) {
189
392
  }
190
393
 
191
394
  if (!old_children.empty()) {
192
- invalidate_node_cache_batch(ctx, rctx, old_children);
395
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
193
396
  if (has_observers) rctx->mutation_observers->disconnectDetachedTargets(old_children);
194
397
  }
195
398
 
196
- get_doc(ctx)->setInnerHTMLOnEl(el, html);
399
+ doc_for_node(ctx, el)->setInnerHTMLOnEl(el, html);
197
400
  JS_FreeCString(ctx, html);
198
401
 
199
402
  if (has_observers) {
@@ -223,16 +426,8 @@ JSValue js_el_get_parentElement(JSContext* ctx, JSValue this_val) {
223
426
 
224
427
  JSValue js_el_get_children(JSContext* ctx, JSValue this_val) {
225
428
  auto* el = unwrap_element(ctx, this_val);
226
- JSValue arr = JS_NewArray(ctx);
227
- if (!el) return arr;
228
- uint32_t idx = 0;
229
- lxb_dom_node_t* child = lxb_dom_interface_node(el)->first_child;
230
- while (child) {
231
- if (child->type == LXB_DOM_NODE_TYPE_ELEMENT)
232
- JS_SetPropertyUint32(ctx, arr, idx++, make_element(ctx, lxb_dom_interface_element(child)));
233
- child = child->next;
234
- }
235
- return arr;
429
+ if (!el) return JS_NewArray(ctx);
430
+ return LiveCollectionBindings::makeChildren(ctx, el);
236
431
  }
237
432
 
238
433
  JSValue js_el_get_firstElementChild(JSContext* ctx, JSValue this_val) {
@@ -346,14 +541,20 @@ JSValue js_el_set_nodeValue(JSContext* ctx, JSValue this_val, JSValue val) {
346
541
  return JS_UNDEFINED;
347
542
  }
348
543
 
544
+ JSValue js_el_get_data_length(JSContext* ctx, JSValue this_val) {
545
+ auto* node = unwrap_node(ctx, this_val);
546
+ if (!node) return JS_UNDEFINED;
547
+ if (node->type != LXB_DOM_NODE_TYPE_TEXT && node->type != LXB_DOM_NODE_TYPE_COMMENT) return JS_UNDEFINED;
548
+ JSValue str_val = js_el_get_nodeValue(ctx, this_val);
549
+ JSValue len_val = JS_GetPropertyStr(ctx, str_val, "length");
550
+ JS_FreeValue(ctx, str_val);
551
+ return len_val;
552
+ }
553
+
349
554
  JSValue js_el_get_childNodes(JSContext* ctx, JSValue this_val) {
350
555
  auto* node = unwrap_node(ctx, this_val);
351
- JSValue arr = JS_NewArray(ctx);
352
- if (!node) return arr;
353
- uint32_t idx = 0;
354
- lxb_dom_node_t* child = node->first_child;
355
- while (child) { JS_SetPropertyUint32(ctx, arr, idx++, make_element(ctx, child)); child = child->next; }
356
- return arr;
556
+ if (!node) return JS_NewArray(ctx);
557
+ return LiveCollectionBindings::makeChildNodes(ctx, node);
357
558
  }
358
559
 
359
560
  JSValue js_el_get_firstChild(JSContext* ctx, JSValue this_val) {
@@ -471,6 +672,93 @@ JSValue js_el_hasAttribute(JSContext* ctx, JSValue this_val, int argc, JSValue*
471
672
  return JS_NewBool(ctx, has);
472
673
  }
473
674
 
675
+ // ── Namespaced attribute methods (getAttributeNS/setAttributeNS/...) ─────────
676
+ // Simplification: this sandbox doesn't track a real (namespace, localName)
677
+ // identity per attribute the way createElementNS()/namespaceURI() do for
678
+ // elements — building that would mean hand-constructing lxb_dom_attr_t nodes
679
+ // with their own ns/prefix fields, a much larger and riskier lift than the
680
+ // element side. Instead, attributes are matched by local name only (the
681
+ // qualified name with any "prefix:" stripped), which is spec-accurate for
682
+ // the common real-world case (a single xlink:href-style attribute) but not
683
+ // for two attributes sharing a local name across different namespaces — a
684
+ // case that essentially never comes up in embedded-widget scripts.
685
+ lxb_dom_attr_t* find_attr_by_local_name(lxb_dom_element_t* el, const std::string& localName) {
686
+ for (lxb_dom_attr_t* attr = lxb_dom_element_first_attribute(el); attr; attr = lxb_dom_element_next_attribute(attr)) {
687
+ size_t len = 0;
688
+ const lxb_char_t* qname = lxb_dom_attr_qualified_name(attr, &len);
689
+ if (!qname) continue;
690
+ std::string q(reinterpret_cast<const char*>(qname), len);
691
+ auto colon = q.find(':');
692
+ std::string local = (colon == std::string::npos) ? q : q.substr(colon + 1);
693
+ if (local == localName) return attr;
694
+ }
695
+ return nullptr;
696
+ }
697
+
698
+ JSValue js_el_getAttributeNS(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
699
+ auto* el = unwrap_element(ctx, this_val);
700
+ if (!el || argc < 2) return JS_NULL;
701
+ const char* local = JS_ToCString(ctx, argv[1]);
702
+ if (!local) return JS_NULL;
703
+ auto* attr = find_attr_by_local_name(el, local);
704
+ JS_FreeCString(ctx, local);
705
+ if (!attr) return JS_NULL;
706
+ size_t len = 0;
707
+ const lxb_char_t* val = lxb_dom_attr_value(attr, &len);
708
+ return val ? JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len) : JS_NewString(ctx, "");
709
+ }
710
+
711
+ JSValue js_el_hasAttributeNS(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
712
+ auto* el = unwrap_element(ctx, this_val);
713
+ if (!el || argc < 2) return JS_FALSE;
714
+ const char* local = JS_ToCString(ctx, argv[1]);
715
+ if (!local) return JS_FALSE;
716
+ bool has = find_attr_by_local_name(el, local) != nullptr;
717
+ JS_FreeCString(ctx, local);
718
+ return JS_NewBool(ctx, has);
719
+ }
720
+
721
+ JSValue js_el_setAttributeNS(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
722
+ auto* el = unwrap_element(ctx, this_val);
723
+ if (!el || argc < 3) return JS_UNDEFINED;
724
+ const char* qualified_name = JS_ToCString(ctx, argv[1]);
725
+ const char* value = JS_ToCString(ctx, argv[2]);
726
+ if (qualified_name && value) {
727
+ std::string qn(qualified_name);
728
+ auto colon = qn.find(':');
729
+ std::string local = (colon == std::string::npos) ? qn : qn.substr(colon + 1);
730
+
731
+ auto* existing = find_attr_by_local_name(el, local);
732
+ if (existing) {
733
+ lxb_dom_attr_set_existing_value(existing,
734
+ reinterpret_cast<const lxb_char_t*>(value), strlen(value));
735
+ } else {
736
+ lxb_dom_element_set_attribute(el,
737
+ reinterpret_cast<const lxb_char_t*>(qualified_name), qn.size(),
738
+ reinterpret_cast<const lxb_char_t*>(value), strlen(value));
739
+ }
740
+ }
741
+ if (qualified_name) JS_FreeCString(ctx, qualified_name);
742
+ if (value) JS_FreeCString(ctx, value);
743
+ return JS_UNDEFINED;
744
+ }
745
+
746
+ JSValue js_el_removeAttributeNS(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
747
+ auto* el = unwrap_element(ctx, this_val);
748
+ if (!el || argc < 2) return JS_UNDEFINED;
749
+ const char* local = JS_ToCString(ctx, argv[1]);
750
+ if (local) {
751
+ auto* attr = find_attr_by_local_name(el, local);
752
+ if (attr) {
753
+ size_t qlen = 0;
754
+ const lxb_char_t* qname = lxb_dom_attr_qualified_name(attr, &qlen);
755
+ if (qname) lxb_dom_element_remove_attribute(el, qname, qlen);
756
+ }
757
+ JS_FreeCString(ctx, local);
758
+ }
759
+ return JS_UNDEFINED;
760
+ }
761
+
474
762
  // Inserts `node` via `insert_one`, expanding DocumentFragments into their
475
763
  // children first (per the DOM's "insert a node" algorithm — a fragment is
476
764
  // never itself part of the resulting tree, only its children are moved in).
@@ -551,8 +839,7 @@ JSValue js_el_insertBefore(JSContext* ctx, JSValue this_val, int argc, JSValue*
551
839
  if (ref) {
552
840
  lxb_dom_node_t* receiver_node = unwrap_node(ctx, this_val);
553
841
  if (!receiver_node || ref->parent != receiver_node) {
554
- JS_ThrowTypeError(ctx, "NotFoundError: reference node is not a child of this node");
555
- return JS_EXCEPTION;
842
+ return throw_dom_exception(ctx, "NotFoundError", "reference node is not a child of this node");
556
843
  }
557
844
  parent_node = receiver_node;
558
845
  inserted = expand_and_insert(new_node, [&](lxb_dom_node_t* n) {
@@ -619,8 +906,7 @@ JSValue js_el_replaceChild(JSContext* ctx, JSValue this_val, int argc, JSValue*
619
906
  auto* old_node = unwrap_node(ctx, argv[1]);
620
907
  if (!new_node || !old_node) return JS_NULL;
621
908
  if (old_node->parent != parent_node) {
622
- JS_ThrowTypeError(ctx, "NotFoundError: the node to be replaced is not a child of this node");
623
- return JS_EXCEPTION;
909
+ return throw_dom_exception(ctx, "NotFoundError", "the node to be replaced is not a child of this node");
624
910
  }
625
911
 
626
912
  lxb_dom_node_t* prev_sib = old_node->prev;
@@ -643,12 +929,15 @@ JSValue js_el_replaceChild(JSContext* ctx, JSValue this_val, int argc, JSValue*
643
929
  return JS_DupValue(ctx, argv[1]); // returns the replaced (old) child, per spec
644
930
  }
645
931
 
646
- lxb_dom_node_t* js_to_node_or_text(JSContext* ctx, JSValue val) {
932
+ // `context_node` supplies the owning document for a string argument coerced
933
+ // into a new text node (see doc_for_node()) — irrelevant when `val` is
934
+ // already a node.
935
+ lxb_dom_node_t* js_to_node_or_text(JSContext* ctx, JSValue val, lxb_dom_node_t* context_node) {
647
936
  auto* node = unwrap_node(ctx, val);
648
937
  if (node) return node;
649
938
  const char* str = JS_ToCString(ctx, val);
650
939
  if (!str) return nullptr;
651
- void* text = get_doc(ctx)->createTextNode(str);
940
+ void* text = doc_for_node(ctx, context_node)->createTextNode(str);
652
941
  JS_FreeCString(ctx, str);
653
942
  return static_cast<lxb_dom_node_t*>(text);
654
943
  }
@@ -660,7 +949,7 @@ JSValue js_el_before(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
660
949
 
661
950
  std::vector<void*> inserted;
662
951
  for (int i = 0; i < argc; i++) {
663
- lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i]);
952
+ lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i], node);
664
953
  if (!n) continue;
665
954
  auto batch = expand_and_insert(n, [&](lxb_dom_node_t* one) { lxb_dom_node_insert_before(node, one); });
666
955
  inserted.insert(inserted.end(), batch.begin(), batch.end());
@@ -682,7 +971,7 @@ JSValue js_el_after(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
682
971
 
683
972
  std::vector<void*> inserted;
684
973
  for (int i = 0; i < argc; i++) {
685
- lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i]);
974
+ lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i], node);
686
975
  if (!n) continue;
687
976
  auto batch = expand_and_insert(n, [&](lxb_dom_node_t* one) {
688
977
  if (ref) lxb_dom_node_insert_before(ref, one); else lxb_dom_node_insert_child(parent, one);
@@ -706,7 +995,7 @@ JSValue js_el_replaceWith(JSContext* ctx, JSValue this_val, int argc, JSValue* a
706
995
 
707
996
  std::vector<void*> inserted;
708
997
  for (int i = 0; i < argc; i++) {
709
- lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i]);
998
+ lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i], node);
710
999
  if (!n) continue;
711
1000
  auto batch = expand_and_insert(n, [&](lxb_dom_node_t* one) { lxb_dom_node_insert_before(node, one); });
712
1001
  inserted.insert(inserted.end(), batch.begin(), batch.end());
@@ -728,7 +1017,7 @@ JSValue js_el_append(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
728
1017
 
729
1018
  std::vector<void*> inserted;
730
1019
  for (int i = 0; i < argc; i++) {
731
- lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i]);
1020
+ lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i], parent_node);
732
1021
  if (!n) continue;
733
1022
  auto batch = expand_and_insert(n, [&](lxb_dom_node_t* one) { lxb_dom_node_insert_child(parent_node, one); });
734
1023
  inserted.insert(inserted.end(), batch.begin(), batch.end());
@@ -749,7 +1038,7 @@ JSValue js_el_prepend(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
749
1038
 
750
1039
  std::vector<void*> inserted;
751
1040
  for (int i = 0; i < argc; i++) {
752
- lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i]);
1041
+ lxb_dom_node_t* n = js_to_node_or_text(ctx, argv[i], parent_node);
753
1042
  if (!n) continue;
754
1043
  auto batch = expand_and_insert(n, [&](lxb_dom_node_t* one) {
755
1044
  if (ref) lxb_dom_node_insert_before(ref, one); else lxb_dom_node_insert_child(parent_node, one);
@@ -770,11 +1059,12 @@ JSValue js_el_closest(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
770
1059
  const char* sel = JS_ToCString(ctx, argv[0]);
771
1060
  if (!sel) return JS_NULL;
772
1061
 
1062
+ auto* doc = doc_for_node(ctx, lxb_dom_interface_node(el));
773
1063
  JSValue result = JS_NULL;
774
1064
  for (lxb_dom_node_t* node = lxb_dom_interface_node(el);
775
1065
  node && node->type == LXB_DOM_NODE_TYPE_ELEMENT;
776
1066
  node = node->parent) {
777
- if (get_doc(ctx)->matchesSelector(lxb_dom_interface_element(node), sel)) {
1067
+ if (doc->matchesSelector(lxb_dom_interface_element(node), sel)) {
778
1068
  result = make_element(ctx, node);
779
1069
  break;
780
1070
  }
@@ -797,7 +1087,7 @@ JSValue js_el_insertAdjacentHTML(JSContext* ctx, JSValue this_val, int argc, JSV
797
1087
  JS_FreeCString(ctx, position);
798
1088
 
799
1089
  lxb_dom_node_t* node = lxb_dom_interface_node(el);
800
- auto parsed = get_doc(ctx)->parseFragmentNodes(el, html);
1090
+ auto parsed = doc_for_node(ctx, node)->parseFragmentNodes(el, html);
801
1091
  JS_FreeCString(ctx, html);
802
1092
  if (parsed.empty()) return JS_UNDEFINED;
803
1093
 
@@ -835,8 +1125,7 @@ JSValue js_el_insertAdjacentHTML(JSContext* ctx, JSValue this_val, int argc, JSV
835
1125
  next_sib = ref;
836
1126
  } else {
837
1127
  for (void* n : parsed) lxb_dom_node_destroy_deep(static_cast<lxb_dom_node_t*>(n));
838
- JS_ThrowTypeError(ctx, "SyntaxError: invalid insertAdjacentHTML position '%s'", pos.c_str());
839
- return JS_EXCEPTION;
1128
+ return throw_dom_exception(ctx, "SyntaxError", ("invalid insertAdjacentHTML position '" + pos + "'").c_str());
840
1129
  }
841
1130
 
842
1131
  auto* rctx = get_ctx(ctx);
@@ -851,7 +1140,7 @@ JSValue js_el_matches(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
851
1140
  if (!el || argc < 1) return JS_FALSE;
852
1141
  const char* sel = JS_ToCString(ctx, argv[0]);
853
1142
  if (!sel) return JS_FALSE;
854
- bool result = get_doc(ctx)->matchesSelector(el, sel);
1143
+ bool result = doc_for_node(ctx, el)->matchesSelector(el, sel);
855
1144
  JS_FreeCString(ctx, sel);
856
1145
  return JS_NewBool(ctx, result);
857
1146
  }
@@ -861,7 +1150,7 @@ JSValue js_el_querySelector(JSContext* ctx, JSValue this_val, int argc, JSValue*
861
1150
  if (!el || argc < 1) return JS_NULL;
862
1151
  const char* sel = JS_ToCString(ctx, argv[0]);
863
1152
  if (!sel) return JS_NULL;
864
- void* found = get_doc(ctx)->querySelectorFromEl(el, sel);
1153
+ void* found = doc_for_node(ctx, el)->querySelectorFromEl(el, sel);
865
1154
  JS_FreeCString(ctx, sel);
866
1155
  return make_element(ctx, found);
867
1156
  }
@@ -871,7 +1160,7 @@ JSValue js_el_querySelectorAll(JSContext* ctx, JSValue this_val, int argc, JSVal
871
1160
  if (!el || argc < 1) return JS_NewArray(ctx);
872
1161
  const char* sel = JS_ToCString(ctx, argv[0]);
873
1162
  if (!sel) return JS_NewArray(ctx);
874
- auto results = get_doc(ctx)->querySelectorAllFromEl(el, sel);
1163
+ auto results = doc_for_node(ctx, el)->querySelectorAllFromEl(el, sel);
875
1164
  JS_FreeCString(ctx, sel);
876
1165
  return make_element_array(ctx, results);
877
1166
  }
@@ -881,9 +1170,9 @@ JSValue js_el_getElementsByClassName(JSContext* ctx, JSValue this_val, int argc,
881
1170
  if (!el || argc < 1) return JS_NewArray(ctx);
882
1171
  const char* names = JS_ToCString(ctx, argv[0]);
883
1172
  if (!names) return JS_NewArray(ctx);
884
- auto results = get_doc(ctx)->querySelectorAllFromEl(el, classNames_to_selector(names));
1173
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, el, classNames_to_selector(names));
885
1174
  JS_FreeCString(ctx, names);
886
- return make_element_array(ctx, results);
1175
+ return result;
887
1176
  }
888
1177
 
889
1178
  JSValue js_el_getElementsByTagName(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
@@ -891,9 +1180,9 @@ JSValue js_el_getElementsByTagName(JSContext* ctx, JSValue this_val, int argc, J
891
1180
  if (!el || argc < 1) return JS_NewArray(ctx);
892
1181
  const char* tag = JS_ToCString(ctx, argv[0]);
893
1182
  if (!tag) return JS_NewArray(ctx);
894
- auto results = get_doc(ctx)->querySelectorAllFromEl(el, tag);
1183
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, el, tag);
895
1184
  JS_FreeCString(ctx, tag);
896
- return make_element_array(ctx, results);
1185
+ return result;
897
1186
  }
898
1187
 
899
1188
  JSValue js_el_getAttributeNames(JSContext* ctx, JSValue this_val, int, JSValue*) {
@@ -966,6 +1255,25 @@ JSValue js_el_toggleAttribute(JSContext* ctx, JSValue this_val, int argc, JSValu
966
1255
  return JS_NewBool(ctx, force_present);
967
1256
  }
968
1257
 
1258
+ JSValue js_el_getBoundingClientRect(JSContext* ctx, JSValue this_val, int, JSValue*) {
1259
+ auto* el = unwrap_element(ctx, this_val);
1260
+ if (!el) return JS_NULL;
1261
+ JSValue rect = JS_NewObject(ctx);
1262
+ static const char* kFields[] = { "x", "y", "width", "height", "top", "right", "bottom", "left" };
1263
+ for (const char* field : kFields) {
1264
+ JS_SetPropertyStr(ctx, rect, field, JS_NewInt32(ctx, 0));
1265
+ }
1266
+ return rect;
1267
+ }
1268
+
1269
+ JSValue js_el_getClientRects(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1270
+ auto* el = unwrap_element(ctx, this_val);
1271
+ if (!el) return JS_NewArray(ctx);
1272
+ JSValue arr = JS_NewArray(ctx);
1273
+ JS_SetPropertyUint32(ctx, arr, 0, js_el_getBoundingClientRect(ctx, this_val, argc, argv));
1274
+ return arr;
1275
+ }
1276
+
969
1277
  JSValue js_el_isSameNode(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
970
1278
  auto* node = unwrap_node(ctx, this_val);
971
1279
  if (!node || argc < 1) return JS_FALSE;
@@ -1022,6 +1330,105 @@ JSValue js_el_isEqualNode(JSContext* ctx, JSValue this_val, int argc, JSValue* a
1022
1330
  return JS_NewBool(ctx, nodes_equal(node, other));
1023
1331
  }
1024
1332
 
1333
+ void collect_text_descendants(lxb_dom_node_t* node, std::vector<void*>& out) {
1334
+ for (lxb_dom_node_t* child = node->first_child; child; child = child->next) {
1335
+ if (child->type == LXB_DOM_NODE_TYPE_TEXT) out.push_back(child);
1336
+ else if (child->first_child) collect_text_descendants(child, out);
1337
+ }
1338
+ }
1339
+
1340
+ JSValue js_el_normalize(JSContext* ctx, JSValue this_val, int, JSValue*) {
1341
+ auto* node = unwrap_node(ctx, this_val);
1342
+ if (!node) return JS_UNDEFINED;
1343
+
1344
+ // normalize() only merges/drops Text nodes (leaves, no children of their
1345
+ // own), so a flat invalidate is enough — no need for the *_deep variant.
1346
+ std::vector<void*> text_nodes;
1347
+ collect_text_descendants(node, text_nodes);
1348
+ if (!text_nodes.empty()) {
1349
+ invalidate_node_cache_batch(ctx, get_ctx(ctx), text_nodes);
1350
+ }
1351
+
1352
+ doc_for_node(ctx, node)->normalize(node);
1353
+ return JS_UNDEFINED;
1354
+ }
1355
+
1356
+ JSValue js_el_compareDocumentPosition(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1357
+ auto* node = unwrap_node(ctx, this_val);
1358
+ if (!node || argc < 1) return JS_NewInt32(ctx, 0);
1359
+ auto* other = unwrap_node(ctx, argv[0]);
1360
+ if (!other) return JS_NewInt32(ctx, 0);
1361
+ return JS_NewInt32(ctx, doc_for_node(ctx, node)->compareDocumentPosition(node, other));
1362
+ }
1363
+
1364
+ JSValue js_canonicalize_root_node(JSContext* ctx, JSValue, int argc, JSValue* argv) {
1365
+ if (argc < 1) return JS_UNDEFINED;
1366
+ auto* node = unwrap_node(ctx, argv[0]);
1367
+ if (node && node->type == LXB_DOM_NODE_TYPE_DOCUMENT && doc_for_node(ctx, node) == get_doc(ctx)) {
1368
+ JSValue global = JS_GetGlobalObject(ctx);
1369
+ JSValue doc_val = JS_GetPropertyStr(ctx, global, "document");
1370
+ JS_FreeValue(ctx, global);
1371
+ return doc_val;
1372
+ }
1373
+ return JS_DupValue(ctx, argv[0]);
1374
+ }
1375
+
1376
+ JSValue js_el_get_ownerDocument(JSContext* ctx, JSValue this_val) {
1377
+ auto* node = unwrap_node(ctx, this_val);
1378
+ if (!node) return JS_NULL;
1379
+ if (node->type == LXB_DOM_NODE_TYPE_DOCUMENT) return JS_NULL;
1380
+
1381
+ LexborDocument* owner = doc_for_node(ctx, node);
1382
+ if (owner == get_doc(ctx)) {
1383
+ JSValue global = JS_GetGlobalObject(ctx);
1384
+ JSValue doc_val = JS_GetPropertyStr(ctx, global, "document");
1385
+ JS_FreeValue(ctx, global);
1386
+ return doc_val;
1387
+ }
1388
+
1389
+ JSValue wrapper = get_document_wrapper(ctx, owner);
1390
+ if (!JS_IsUndefined(wrapper)) return wrapper;
1391
+
1392
+ void* html_doc = owner->documentHtmlPtr();
1393
+ void* doc_node = lxb_dom_interface_node(
1394
+ lxb_dom_interface_document(static_cast<lxb_html_document_t*>(html_doc)));
1395
+ return make_element(ctx, doc_node);
1396
+ }
1397
+
1398
+ JSValue js_el_get_namespaceURI(JSContext* ctx, JSValue this_val) {
1399
+ auto* node = unwrap_node(ctx, this_val);
1400
+ if (!node) return JS_NULL;
1401
+ LexborDocument* owner = doc_for_node(ctx, node);
1402
+ std::string uri = owner ? owner->namespaceURI(node) : "";
1403
+ if (uri.empty()) return JS_NULL;
1404
+ return JS_NewStringLen(ctx, uri.data(), uri.size());
1405
+ }
1406
+
1407
+ // Element.prefix — the namespace prefix set via createElementNS('ns', 'prefix:local'),
1408
+ // or null for an element with no prefix (the overwhelmingly common case).
1409
+ JSValue js_el_get_prefix(JSContext* ctx, JSValue this_val) {
1410
+ auto* el = unwrap_element(ctx, this_val);
1411
+ if (!el) return JS_NULL;
1412
+ auto* node = reinterpret_cast<lxb_dom_node_t*>(el);
1413
+ if (node->prefix == 0) return JS_NULL;
1414
+ const lxb_ns_prefix_data_t* data = lxb_ns_prefix_data_by_id(node->owner_document->prefix, node->prefix);
1415
+ if (!data) return JS_NULL;
1416
+ const lxb_char_t* str = lexbor_hash_entry_str(&data->entry);
1417
+ if (!str) return JS_NULL;
1418
+ return JS_NewStringLen(ctx, reinterpret_cast<const char*>(str), data->entry.length);
1419
+ }
1420
+
1421
+ // Node.baseURI — always the document's URL in this sandbox (no <base> element
1422
+ // support/per-node override), so this just forwards to location.href.
1423
+ JSValue js_el_get_baseURI(JSContext* ctx, JSValue) {
1424
+ JSValue global = JS_GetGlobalObject(ctx);
1425
+ JSValue location = JS_GetPropertyStr(ctx, global, "location");
1426
+ JS_FreeValue(ctx, global);
1427
+ JSValue href = JS_GetPropertyStr(ctx, location, "href");
1428
+ JS_FreeValue(ctx, location);
1429
+ return href;
1430
+ }
1431
+
1025
1432
  } // namespace
1026
1433
 
1027
1434
  void ElementBindings::install(JSContext* ctx) {
@@ -1048,11 +1455,18 @@ void ElementBindings::install(JSContext* ctx) {
1048
1455
  JS_SetPropertyStr(ctx, node_proto, "replaceWith", JS_NewCFunction(ctx, js_el_replaceWith, "replaceWith", 0));
1049
1456
  JS_SetPropertyStr(ctx, node_proto, "isSameNode", JS_NewCFunction(ctx, js_el_isSameNode, "isSameNode", 1));
1050
1457
  JS_SetPropertyStr(ctx, node_proto, "isEqualNode", JS_NewCFunction(ctx, js_el_isEqualNode, "isEqualNode", 1));
1458
+ JS_SetPropertyStr(ctx, node_proto, "normalize", JS_NewCFunction(ctx, js_el_normalize, "normalize", 0));
1459
+ JS_SetPropertyStr(ctx, node_proto, "compareDocumentPosition", JS_NewCFunction(ctx, js_el_compareDocumentPosition, "compareDocumentPosition", 1));
1051
1460
 
1052
1461
  define_prop(ctx, node_proto, "nodeType", js_el_get_nodeType, nullptr);
1053
1462
  define_prop(ctx, node_proto, "nodeName", js_el_get_nodeName, nullptr);
1054
1463
  define_prop(ctx, node_proto, "nodeValue", js_el_get_nodeValue, js_el_set_nodeValue);
1464
+ define_prop(ctx, node_proto, "data", js_el_get_nodeValue, js_el_set_nodeValue);
1465
+ define_prop(ctx, node_proto, "length", js_el_get_data_length, nullptr);
1055
1466
  define_prop(ctx, node_proto, "textContent", js_el_get_textContent, js_el_set_textContent);
1467
+ define_prop(ctx, node_proto, "ownerDocument", js_el_get_ownerDocument, nullptr);
1468
+ define_prop(ctx, node_proto, "namespaceURI", js_el_get_namespaceURI, nullptr);
1469
+ define_prop(ctx, node_proto, "baseURI", js_el_get_baseURI, nullptr);
1056
1470
  define_prop(ctx, node_proto, "childNodes", js_el_get_childNodes, nullptr);
1057
1471
  define_prop(ctx, node_proto, "firstChild", js_el_get_firstChild, nullptr);
1058
1472
  define_prop(ctx, node_proto, "lastChild", js_el_get_lastChild, nullptr);
@@ -1061,6 +1475,8 @@ void ElementBindings::install(JSContext* ctx) {
1061
1475
  define_prop(ctx, node_proto, "parentNode", js_el_get_parentNode, nullptr);
1062
1476
  define_prop(ctx, node_proto, "parentElement", js_el_get_parentElement, nullptr);
1063
1477
 
1478
+ define_node_type_constants(ctx, node_proto);
1479
+
1064
1480
  JS_SetClassProto(ctx, js_node_class_id, JS_DupValue(ctx, node_proto));
1065
1481
 
1066
1482
  // ── Element proto: inherits Node, adds Element-specific APIs ─────────────
@@ -1071,10 +1487,15 @@ void ElementBindings::install(JSContext* ctx) {
1071
1487
  JS_SetPropertyStr(ctx, proto, "setAttribute", JS_NewCFunction(ctx, js_el_setAttribute, "setAttribute", 2));
1072
1488
  JS_SetPropertyStr(ctx, proto, "removeAttribute", JS_NewCFunction(ctx, js_el_removeAttribute, "removeAttribute", 1));
1073
1489
  JS_SetPropertyStr(ctx, proto, "hasAttribute", JS_NewCFunction(ctx, js_el_hasAttribute, "hasAttribute", 1));
1490
+ JS_SetPropertyStr(ctx, proto, "getAttributeNS", JS_NewCFunction(ctx, js_el_getAttributeNS, "getAttributeNS", 2));
1491
+ JS_SetPropertyStr(ctx, proto, "setAttributeNS", JS_NewCFunction(ctx, js_el_setAttributeNS, "setAttributeNS", 3));
1492
+ JS_SetPropertyStr(ctx, proto, "removeAttributeNS", JS_NewCFunction(ctx, js_el_removeAttributeNS, "removeAttributeNS", 2));
1493
+ JS_SetPropertyStr(ctx, proto, "hasAttributeNS", JS_NewCFunction(ctx, js_el_hasAttributeNS, "hasAttributeNS", 2));
1074
1494
  JS_SetPropertyStr(ctx, proto, "toggleAttribute", JS_NewCFunction(ctx, js_el_toggleAttribute, "toggleAttribute", 2));
1075
1495
  JS_SetPropertyStr(ctx, proto, "getAttributeNames", JS_NewCFunction(ctx, js_el_getAttributeNames, "getAttributeNames", 0));
1076
1496
  JS_SetPropertyStr(ctx, proto, "remove", JS_NewCFunction(ctx, js_el_remove, "remove", 0));
1077
1497
  JS_SetPropertyStr(ctx, proto, "matches", JS_NewCFunction(ctx, js_el_matches, "matches", 1));
1498
+ JS_SetPropertyStr(ctx, proto, "webkitMatchesSelector", JS_NewCFunction(ctx, js_el_matches, "webkitMatchesSelector", 1));
1078
1499
  JS_SetPropertyStr(ctx, proto, "querySelector", JS_NewCFunction(ctx, js_el_querySelector, "querySelector", 1));
1079
1500
  JS_SetPropertyStr(ctx, proto, "querySelectorAll", JS_NewCFunction(ctx, js_el_querySelectorAll, "querySelectorAll", 1));
1080
1501
  JS_SetPropertyStr(ctx, proto, "getElementsByClassName", JS_NewCFunction(ctx, js_el_getElementsByClassName, "getElementsByClassName", 1));
@@ -1083,12 +1504,18 @@ void ElementBindings::install(JSContext* ctx) {
1083
1504
  JS_SetPropertyStr(ctx, proto, "insertAdjacentHTML", JS_NewCFunction(ctx, js_el_insertAdjacentHTML, "insertAdjacentHTML", 2));
1084
1505
  JS_SetPropertyStr(ctx, proto, "append", JS_NewCFunction(ctx, js_el_append, "append", 0));
1085
1506
  JS_SetPropertyStr(ctx, proto, "prepend", JS_NewCFunction(ctx, js_el_prepend, "prepend", 0));
1507
+ JS_SetPropertyStr(ctx, proto, "getBoundingClientRect", JS_NewCFunction(ctx, js_el_getBoundingClientRect, "getBoundingClientRect", 0));
1508
+ JS_SetPropertyStr(ctx, proto, "getClientRects", JS_NewCFunction(ctx, js_el_getClientRects, "getClientRects", 0));
1086
1509
 
1087
1510
  define_prop(ctx, proto, "tagName", js_el_get_tagName, nullptr);
1511
+ define_prop(ctx, proto, "prefix", js_el_get_prefix, nullptr);
1088
1512
  define_prop(ctx, proto, "id", js_el_get_id, js_el_set_id);
1089
1513
  define_prop(ctx, proto, "className", js_el_get_className, js_el_set_className);
1514
+ define_prop(ctx, proto, "value", js_el_get_value, js_el_set_value);
1515
+ define_prop(ctx, proto, "checked", js_el_get_checked, js_el_set_checked);
1090
1516
  define_prop(ctx, proto, "innerHTML", js_el_get_innerHTML, js_el_set_innerHTML);
1091
1517
  define_prop(ctx, proto, "outerHTML", js_el_get_outerHTML, nullptr);
1518
+ define_prop(ctx, proto, "innerText", js_el_get_textContent, js_el_set_textContent);
1092
1519
  define_prop(ctx, proto, "children", js_el_get_children, nullptr);
1093
1520
  define_prop(ctx, proto, "firstElementChild", js_el_get_firstElementChild, nullptr);
1094
1521
  define_prop(ctx, proto, "lastElementChild", js_el_get_lastElementChild, nullptr);
@@ -1101,6 +1528,91 @@ void ElementBindings::install(JSContext* ctx) {
1101
1528
  define_prop(ctx, proto, "attributes", js_el_get_attributes, nullptr);
1102
1529
 
1103
1530
  JS_SetClassProto(ctx, js_element_class_id, proto);
1531
+
1532
+ JSValue node_proto_ref = JS_GetClassProto(ctx, js_node_class_id);
1533
+ JSValue element_proto_ref = JS_GetClassProto(ctx, js_element_class_id);
1534
+
1535
+ JSValue node_ctor = define_global_constructor(ctx, "Node", node_proto_ref);
1536
+ define_node_type_constants(ctx, node_ctor);
1537
+ JS_FreeValue(ctx, node_ctor);
1538
+ JSValue element_ctor = define_global_constructor(ctx, "Element", element_proto_ref);
1539
+ JSValue global = JS_GetGlobalObject(ctx);
1540
+ JS_SetPropertyStr(ctx, global, "HTMLElement", element_ctor);
1541
+ JS_SetPropertyStr(ctx, global, "__nativeCanonicalizeRootNode",
1542
+ JS_NewCFunction(ctx, js_canonicalize_root_node, "__nativeCanonicalizeRootNode", 1));
1543
+ JS_FreeValue(ctx, global);
1544
+
1545
+ JS_FreeValue(ctx, node_proto_ref);
1546
+ JS_FreeValue(ctx, element_proto_ref);
1547
+
1548
+ static const char* kGetRootNodeScript = R"JS(
1549
+ (function() {
1550
+ Node.prototype.getRootNode = function(options) {
1551
+ var composed = !!(options && options.composed);
1552
+ var node = this;
1553
+ while (true) {
1554
+ var parent = node.parentNode;
1555
+ if (parent) { node = parent; continue; }
1556
+ if (composed && node.host) { node = node.host; continue; }
1557
+ return __nativeCanonicalizeRootNode(node);
1558
+ }
1559
+ };
1560
+
1561
+ Object.defineProperty(Node.prototype, 'isConnected', {
1562
+ get: function() { return this.getRootNode() === document; },
1563
+ enumerable: true,
1564
+ configurable: true,
1565
+ });
1566
+
1567
+ function locateNamespace(node, prefix) {
1568
+ if (!node) return null;
1569
+ if (node.nodeType === 1) {
1570
+ if (node.namespaceURI !== null && node.prefix === prefix) return node.namespaceURI;
1571
+ var attrs = node.attributes;
1572
+ for (var i = 0; i < attrs.length; i++) {
1573
+ var a = attrs[i];
1574
+ if (prefix === null && a.name === 'xmlns') return a.value || null;
1575
+ if (prefix !== null && a.name === 'xmlns:' + prefix) return a.value || null;
1576
+ }
1577
+ return locateNamespace(node.parentElement, prefix);
1578
+ }
1579
+ if (node.nodeType === 9) {
1580
+ return node.documentElement ? locateNamespace(node.documentElement, prefix) : null;
1581
+ }
1582
+ if (node.nodeType === 10 || node.nodeType === 11) return null;
1583
+ return locateNamespace(node.parentElement, prefix);
1584
+ }
1585
+
1586
+ Node.prototype.lookupNamespaceURI = function(prefix) {
1587
+ if (prefix === undefined || prefix === '') prefix = null;
1588
+ return locateNamespace(this, prefix);
1589
+ };
1590
+
1591
+ Node.prototype.isDefaultNamespace = function(namespace) {
1592
+ if (namespace === undefined || namespace === '') namespace = null;
1593
+ return this.lookupNamespaceURI(null) === namespace;
1594
+ };
1595
+
1596
+ Node.prototype.lookupPrefix = function(namespace) {
1597
+ if (!namespace) return null;
1598
+ var node = this.nodeType === 9 ? this.documentElement : this;
1599
+ while (node && node.nodeType === 1) {
1600
+ if (node.namespaceURI === namespace && node.prefix !== null) return node.prefix;
1601
+ var attrs = node.attributes;
1602
+ for (var i = 0; i < attrs.length; i++) {
1603
+ var a = attrs[i];
1604
+ if (a.name.indexOf('xmlns:') === 0 && a.value === namespace) return a.name.slice(6);
1605
+ }
1606
+ node = node.parentElement;
1607
+ }
1608
+ return null;
1609
+ };
1610
+ })();
1611
+ )JS";
1612
+ JSValue root_node_result = JS_Eval(ctx, kGetRootNodeScript, strlen(kGetRootNodeScript),
1613
+ "<get-root-node-bootstrap>", JS_EVAL_TYPE_GLOBAL);
1614
+ if (JS_IsException(root_node_result)) JS_FreeValue(ctx, JS_GetException(ctx));
1615
+ JS_FreeValue(ctx, root_node_result);
1104
1616
  }
1105
1617
 
1106
1618
  } // namespace margelo::nitro::nitrojsdom