@salve-software/react-native-nitro-jsdom 1.0.0 → 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 (57) hide show
  1. package/README.md +1 -1
  2. package/android/CMakeLists.txt +14 -0
  3. package/cpp/HybridHtmlSandbox.cpp +16 -3
  4. package/cpp/HybridHtmlSandbox.hpp +1 -1
  5. package/cpp/lexbor/LexborDocument.cpp +217 -67
  6. package/cpp/lexbor/LexborDocument.hpp +29 -10
  7. package/cpp/quickjs/DOMBindings.cpp +28 -0
  8. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  9. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  10. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  11. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  12. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  13. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  14. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  15. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  17. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
  18. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  19. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  20. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  22. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  24. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  25. package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
  26. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  27. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  28. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  29. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  30. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  32. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  34. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  35. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  36. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  38. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  40. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  41. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  42. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  43. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  44. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  46. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  48. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  50. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  51. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  53. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  54. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  55. package/package.json +2 -2
  56. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  57. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -1,7 +1,10 @@
1
1
  #include "DocumentBindings.hpp"
2
+ #include "LiveCollectionBindings.hpp"
2
3
  #include "../DOMBindingsInternal.hpp"
4
+ #include "../QuickJSRuntime.hpp"
3
5
  #include "../../lexbor/LexborDocument.hpp"
4
6
  #include <cstring>
7
+ #include <string>
5
8
 
6
9
  namespace margelo::nitro::nitrojsdom {
7
10
 
@@ -38,18 +41,47 @@ JSValue js_doc_getElementsByClassName(JSContext* ctx, JSValue, int argc, JSValue
38
41
  if (argc < 1) return JS_NewArray(ctx);
39
42
  const char* names = JS_ToCString(ctx, argv[0]);
40
43
  if (!names) return JS_NewArray(ctx);
41
- auto results = get_doc(ctx)->querySelectorAll_el(classNames_to_selector(names));
44
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, nullptr, classNames_to_selector(names));
42
45
  JS_FreeCString(ctx, names);
43
- return make_element_array(ctx, results);
46
+ return result;
44
47
  }
45
48
 
46
49
  JSValue js_doc_getElementsByTagName(JSContext* ctx, JSValue, int argc, JSValue* argv) {
47
50
  if (argc < 1) return JS_NewArray(ctx);
48
51
  const char* tag = JS_ToCString(ctx, argv[0]);
49
52
  if (!tag) return JS_NewArray(ctx);
50
- auto results = get_doc(ctx)->querySelectorAll_el(tag);
53
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, nullptr, tag);
51
54
  JS_FreeCString(ctx, tag);
52
- return make_element_array(ctx, results);
55
+ return result;
56
+ }
57
+
58
+ JSValue js_doc_getElementsByName(JSContext* ctx, JSValue, int argc, JSValue* argv) {
59
+ if (argc < 1) return JS_NewArray(ctx);
60
+ const char* name = JS_ToCString(ctx, argv[0]);
61
+ if (!name) return JS_NewArray(ctx);
62
+ std::string selector = "[name=\"" + std::string(name) + "\"]";
63
+ JS_FreeCString(ctx, name);
64
+ return LiveCollectionBindings::makeBySelector(ctx, nullptr, selector);
65
+ }
66
+
67
+ // document.forms/.images/.scripts/.links — live HTMLCollections backed by the
68
+ // same LiveCollectionBindings::makeBySelector primitive that already backs
69
+ // getElementsByClassName/getElementsByTagName. .links matches jsdom: any <a>
70
+ // or <area> that actually has an href attribute (bare <a> anchors don't count).
71
+ JSValue js_doc_get_forms(JSContext* ctx, JSValue) {
72
+ return LiveCollectionBindings::makeBySelector(ctx, nullptr, "form");
73
+ }
74
+
75
+ JSValue js_doc_get_images(JSContext* ctx, JSValue) {
76
+ return LiveCollectionBindings::makeBySelector(ctx, nullptr, "img");
77
+ }
78
+
79
+ JSValue js_doc_get_scripts(JSContext* ctx, JSValue) {
80
+ return LiveCollectionBindings::makeBySelector(ctx, nullptr, "script");
81
+ }
82
+
83
+ JSValue js_doc_get_links(JSContext* ctx, JSValue) {
84
+ return LiveCollectionBindings::makeBySelector(ctx, nullptr, "a[href], area[href]");
53
85
  }
54
86
 
55
87
  JSValue js_doc_createElement(JSContext* ctx, JSValue, int argc, JSValue* argv) {
@@ -96,6 +128,39 @@ JSValue js_doc_get_documentElement(JSContext* ctx, JSValue) {
96
128
  return make_element(ctx, get_doc(ctx)->documentElement());
97
129
  }
98
130
 
131
+ // document.doctype is exposed as a plain {name, publicId, systemId, nodeType,
132
+ // nodeName} object rather than routed through make_element()/the generic
133
+ // Node wrapper: DocumentType's `name` is an interned lxb_dom_attr_id_t (needs
134
+ // LexborDocument::doctypeName(), not the generic nodeName getter), and it has
135
+ // no other Node behavior real-world embedded scripts rely on (no children,
136
+ // not appendChild-able). Since it's a plain object rather than a cached
137
+ // node-pointer-keyed wrapper, the constructed object is cached directly on
138
+ // RuntimeContext (built once, on first access) so repeated `document.doctype`
139
+ // reads return the same JS object — the document itself never gets
140
+ // re-parsed mid-instance, so the cache never needs invalidation.
141
+ JSValue js_doc_get_doctype(JSContext* ctx, JSValue) {
142
+ void* dt = get_doc(ctx)->doctype();
143
+ if (!dt) return JS_NULL;
144
+
145
+ auto* rctx = get_ctx(ctx);
146
+ if (rctx && rctx->doctype_wrapper) {
147
+ return JS_DupValue(ctx, *static_cast<JSValue*>(rctx->doctype_wrapper));
148
+ }
149
+
150
+ JSValue obj = JS_NewObject(ctx);
151
+ std::string name = get_doc(ctx)->doctypeName(dt);
152
+ JS_SetPropertyStr(ctx, obj, "name", JS_NewStringLen(ctx, name.data(), name.size()));
153
+ std::string publicId = get_doc(ctx)->doctypePublicId(dt);
154
+ JS_SetPropertyStr(ctx, obj, "publicId", JS_NewStringLen(ctx, publicId.data(), publicId.size()));
155
+ std::string systemId = get_doc(ctx)->doctypeSystemId(dt);
156
+ JS_SetPropertyStr(ctx, obj, "systemId", JS_NewStringLen(ctx, systemId.data(), systemId.size()));
157
+ JS_SetPropertyStr(ctx, obj, "nodeType", JS_NewInt32(ctx, 10));
158
+ JS_SetPropertyStr(ctx, obj, "nodeName", JS_NewStringLen(ctx, name.data(), name.size()));
159
+
160
+ if (rctx) rctx->doctype_wrapper = new JSValue(JS_DupValue(ctx, obj));
161
+ return obj;
162
+ }
163
+
99
164
  const char* kDocumentTitleBootstrapScript = R"JS(
100
165
  (function() {
101
166
  Object.defineProperty(document, 'title', {
@@ -132,6 +197,7 @@ void DocumentBindings::install(JSContext* ctx) {
132
197
  JS_SetPropertyStr(ctx, doc, "querySelectorAll", JS_NewCFunction(ctx, js_doc_querySelectorAll, "querySelectorAll", 1));
133
198
  JS_SetPropertyStr(ctx, doc, "getElementsByClassName", JS_NewCFunction(ctx, js_doc_getElementsByClassName, "getElementsByClassName", 1));
134
199
  JS_SetPropertyStr(ctx, doc, "getElementsByTagName", JS_NewCFunction(ctx, js_doc_getElementsByTagName, "getElementsByTagName", 1));
200
+ JS_SetPropertyStr(ctx, doc, "getElementsByName", JS_NewCFunction(ctx, js_doc_getElementsByName, "getElementsByName", 1));
135
201
  JS_SetPropertyStr(ctx, doc, "createElement", JS_NewCFunction(ctx, js_doc_createElement, "createElement", 1));
136
202
  JS_SetPropertyStr(ctx, doc, "createTextNode", JS_NewCFunction(ctx, js_doc_createTextNode, "createTextNode", 1));
137
203
  JS_SetPropertyStr(ctx, doc, "createComment", JS_NewCFunction(ctx, js_doc_createComment, "createComment", 1));
@@ -140,6 +206,20 @@ void DocumentBindings::install(JSContext* ctx) {
140
206
  define_prop(ctx, doc, "body", js_doc_get_body, nullptr);
141
207
  define_prop(ctx, doc, "head", js_doc_get_head, nullptr);
142
208
  define_prop(ctx, doc, "documentElement", js_doc_get_documentElement, nullptr);
209
+ define_prop(ctx, doc, "doctype", js_doc_get_doctype, nullptr);
210
+ define_prop(ctx, doc, "forms", js_doc_get_forms, nullptr);
211
+ define_prop(ctx, doc, "images", js_doc_get_images, nullptr);
212
+ define_prop(ctx, doc, "scripts", js_doc_get_scripts, nullptr);
213
+ define_prop(ctx, doc, "links", js_doc_get_links, nullptr);
214
+
215
+ RuntimeContext* rctx = get_ctx(ctx);
216
+ bool hidden = !(rctx && rctx->pretend_to_be_visual);
217
+ JS_SetPropertyStr(ctx, doc, "hidden", JS_NewBool(ctx, hidden));
218
+
219
+ JSValue document_proto = JS_NewObject(ctx);
220
+ JS_SetPrototype(ctx, doc, document_proto);
221
+ JS_FreeValue(ctx, define_global_constructor(ctx, "Document", document_proto));
222
+ JS_FreeValue(ctx, document_proto);
143
223
 
144
224
  JS_SetPropertyStr(ctx, global, "document", doc);
145
225
  JS_FreeValue(ctx, global);
@@ -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"
@@ -100,6 +102,196 @@ JSValue js_el_set_className(JSContext* ctx, JSValue this_val, JSValue val) {
100
102
  return JS_UNDEFINED;
101
103
  }
102
104
 
105
+ // ── Form element properties (value / checked) ────────────────────────────────
106
+ // Simplified attribute-reflection model, matching this file's existing id/
107
+ // className getters: no separate "dirty value" IDL flag like real browsers,
108
+ // just direct reflection so scripts that set then read these back (the
109
+ // common embedded-widget pattern) see consistent results.
110
+
111
+ JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val);
112
+
113
+ bool element_tag_is(lxb_dom_element_t* el, const char* tag) {
114
+ size_t len = 0;
115
+ const lxb_char_t* name = lxb_dom_element_local_name(el, &len);
116
+ if (!name || len != strlen(tag)) return false;
117
+ for (size_t i = 0; i < len; i++) {
118
+ if (std::tolower(name[i]) != std::tolower((unsigned char)tag[i])) return false;
119
+ }
120
+ return true;
121
+ }
122
+
123
+ // HTMLOptionElement's value: the "value" attribute if present, else the
124
+ // option's text content.
125
+ std::string option_effective_value(lxb_dom_element_t* option) {
126
+ size_t len = 0;
127
+ const lxb_char_t* val = lxb_dom_element_get_attribute(option,
128
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
129
+ if (val) return std::string(reinterpret_cast<const char*>(val), len);
130
+ lxb_dom_node_t* node = lxb_dom_interface_node(option);
131
+ lxb_char_t* text = lxb_dom_node_text_content(node, &len);
132
+ if (!text) return "";
133
+ std::string result(reinterpret_cast<char*>(text), len);
134
+ lxb_dom_document_destroy_text(node->owner_document, text);
135
+ return result;
136
+ }
137
+
138
+ JSValue js_select_get_value(JSContext* ctx, lxb_dom_element_t* select_el) {
139
+ auto options = get_doc(ctx)->querySelectorAllFromEl(select_el, "option");
140
+ void* chosen = nullptr;
141
+ for (void* opt : options) {
142
+ if (lxb_dom_element_has_attribute(static_cast<lxb_dom_element_t*>(opt),
143
+ reinterpret_cast<const lxb_char_t*>("selected"), 8)) { chosen = opt; break; }
144
+ }
145
+ if (!chosen && !options.empty()) chosen = options.front();
146
+ if (!chosen) return JS_NewString(ctx, "");
147
+ return JS_NewString(ctx, option_effective_value(static_cast<lxb_dom_element_t*>(chosen)).c_str());
148
+ }
149
+
150
+ void js_select_set_value(JSContext* ctx, lxb_dom_element_t* select_el, const std::string& value) {
151
+ auto options = get_doc(ctx)->querySelectorAllFromEl(select_el, "option");
152
+ auto* rctx = get_ctx(ctx);
153
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
154
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>("selected");
155
+
156
+ for (void* opt : options) {
157
+ auto* opt_el = static_cast<lxb_dom_element_t*>(opt);
158
+ bool should_select = option_effective_value(opt_el) == value;
159
+ bool has_selected = lxb_dom_element_has_attribute(opt_el, attr_name, 8);
160
+
161
+ std::optional<std::string> old_val;
162
+ if (has_obs && has_selected && rctx->mutation_observers->hasAttributeOldValueObserver()) {
163
+ size_t len = 0;
164
+ const lxb_char_t* v = lxb_dom_element_get_attribute(opt_el, attr_name, 8, &len);
165
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
166
+ }
167
+
168
+ bool changed = false;
169
+ if (should_select && !has_selected) {
170
+ lxb_dom_element_set_attribute(opt_el, attr_name, 8, reinterpret_cast<const lxb_char_t*>(""), 0);
171
+ changed = true;
172
+ } else if (!should_select && has_selected) {
173
+ lxb_dom_element_remove_attribute(opt_el, attr_name, 8);
174
+ changed = true;
175
+ }
176
+
177
+ if (changed && has_obs) {
178
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(opt_el), "selected", old_val);
179
+ }
180
+ }
181
+ }
182
+
183
+ JSValue js_el_get_value(JSContext* ctx, JSValue this_val) {
184
+ auto* el = unwrap_element(ctx, this_val);
185
+ if (!el) return JS_NewString(ctx, "");
186
+ if (element_tag_is(el, "select")) return js_select_get_value(ctx, el);
187
+ if (element_tag_is(el, "textarea")) return js_el_get_textContent(ctx, this_val);
188
+ size_t len = 0;
189
+ const lxb_char_t* val = lxb_dom_element_get_attribute(el,
190
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
191
+ if (val) return JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len);
192
+
193
+ // A checkbox/radio with no "value" attribute defaults its value to "on".
194
+ if (element_tag_is(el, "input")) {
195
+ size_t type_len = 0;
196
+ const lxb_char_t* type_val = lxb_dom_element_get_attribute(el,
197
+ reinterpret_cast<const lxb_char_t*>("type"), 4, &type_len);
198
+ std::string type_str = type_val ? std::string(reinterpret_cast<const char*>(type_val), type_len) : "";
199
+ for (auto& c : type_str) c = (char)std::tolower((unsigned char)c);
200
+ if (type_str == "checkbox" || type_str == "radio") return JS_NewString(ctx, "on");
201
+ }
202
+ return JS_NewString(ctx, "");
203
+ }
204
+
205
+ JSValue js_el_set_value(JSContext* ctx, JSValue this_val, JSValue val) {
206
+ auto* el = unwrap_element(ctx, this_val);
207
+ if (!el) return JS_UNDEFINED;
208
+ const char* str = JS_ToCString(ctx, val);
209
+ if (!str) return JS_UNDEFINED;
210
+
211
+ if (element_tag_is(el, "select")) {
212
+ js_select_set_value(ctx, el, str);
213
+ } else if (element_tag_is(el, "textarea")) {
214
+ auto* rctx = get_ctx(ctx);
215
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
216
+ lxb_dom_node_t* node = lxb_dom_interface_node(el);
217
+
218
+ std::vector<void*> old_children;
219
+ lxb_dom_node_t* child = node->first_child;
220
+ while (child) { old_children.push_back(child); child = child->next; }
221
+ if (!old_children.empty()) {
222
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
223
+ if (has_obs) rctx->mutation_observers->disconnectDetachedTargets(old_children);
224
+ }
225
+
226
+ get_doc(ctx)->setTextContentOnEl(el, str);
227
+
228
+ if (has_obs) {
229
+ std::vector<void*> new_children;
230
+ child = node->first_child;
231
+ while (child) { new_children.push_back(child); child = child->next; }
232
+ rctx->mutation_observers->notifyChildList(ctx, node, new_children, {}, nullptr, nullptr);
233
+ }
234
+ } else {
235
+ auto* rctx = get_ctx(ctx);
236
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
237
+ std::optional<std::string> old_val;
238
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver()) {
239
+ size_t len = 0;
240
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el,
241
+ reinterpret_cast<const lxb_char_t*>("value"), 5, &len);
242
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
243
+ }
244
+ lxb_dom_element_set_attribute(el,
245
+ reinterpret_cast<const lxb_char_t*>("value"), 5,
246
+ reinterpret_cast<const lxb_char_t*>(str), strlen(str));
247
+ if (has_obs) {
248
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "value", old_val);
249
+ }
250
+ }
251
+
252
+ JS_FreeCString(ctx, str);
253
+ return JS_UNDEFINED;
254
+ }
255
+
256
+ JSValue js_el_get_checked(JSContext* ctx, JSValue this_val) {
257
+ auto* el = unwrap_element(ctx, this_val);
258
+ if (!el) return JS_FALSE;
259
+ bool has = lxb_dom_element_has_attribute(el,
260
+ reinterpret_cast<const lxb_char_t*>("checked"), 7);
261
+ return JS_NewBool(ctx, has);
262
+ }
263
+
264
+ JSValue js_el_set_checked(JSContext* ctx, JSValue this_val, JSValue val) {
265
+ auto* el = unwrap_element(ctx, this_val);
266
+ if (!el) return JS_UNDEFINED;
267
+ bool checked = JS_ToBool(ctx, val) > 0;
268
+ auto* attr_name = reinterpret_cast<const lxb_char_t*>("checked");
269
+ bool has = lxb_dom_element_has_attribute(el, attr_name, 7);
270
+
271
+ auto* rctx = get_ctx(ctx);
272
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
273
+ std::optional<std::string> old_val;
274
+ if (has_obs && has && rctx->mutation_observers->hasAttributeOldValueObserver()) {
275
+ size_t len = 0;
276
+ const lxb_char_t* v = lxb_dom_element_get_attribute(el, attr_name, 7, &len);
277
+ if (v) old_val = std::string(reinterpret_cast<const char*>(v), len);
278
+ }
279
+
280
+ if (checked && !has) {
281
+ lxb_dom_element_set_attribute(el, attr_name, 7, reinterpret_cast<const lxb_char_t*>(""), 0);
282
+ if (has_obs) {
283
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "checked", std::nullopt);
284
+ }
285
+ } else if (!checked && has) {
286
+ lxb_dom_element_remove_attribute(el, attr_name, 7);
287
+ if (has_obs) {
288
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el), "checked", old_val);
289
+ }
290
+ }
291
+
292
+ return JS_UNDEFINED;
293
+ }
294
+
103
295
  JSValue js_el_get_textContent(JSContext* ctx, JSValue this_val) {
104
296
  lxb_dom_node_t* node = unwrap_node(ctx, this_val);
105
297
  if (!node) return JS_NewString(ctx, "");
@@ -144,7 +336,7 @@ JSValue js_el_set_textContent(JSContext* ctx, JSValue this_val, JSValue val) {
144
336
  lxb_dom_node_t* child = node->first_child;
145
337
  while (child) { old_children.push_back(child); child = child->next; }
146
338
  if (!old_children.empty()) {
147
- invalidate_node_cache_batch(ctx, rctx, old_children);
339
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
148
340
  if (has_observers) rctx->mutation_observers->disconnectDetachedTargets(old_children);
149
341
  }
150
342
  }
@@ -189,7 +381,7 @@ JSValue js_el_set_innerHTML(JSContext* ctx, JSValue this_val, JSValue val) {
189
381
  }
190
382
 
191
383
  if (!old_children.empty()) {
192
- invalidate_node_cache_batch(ctx, rctx, old_children);
384
+ invalidate_node_cache_deep_batch(ctx, rctx, old_children);
193
385
  if (has_observers) rctx->mutation_observers->disconnectDetachedTargets(old_children);
194
386
  }
195
387
 
@@ -223,16 +415,8 @@ JSValue js_el_get_parentElement(JSContext* ctx, JSValue this_val) {
223
415
 
224
416
  JSValue js_el_get_children(JSContext* ctx, JSValue this_val) {
225
417
  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;
418
+ if (!el) return JS_NewArray(ctx);
419
+ return LiveCollectionBindings::makeChildren(ctx, el);
236
420
  }
237
421
 
238
422
  JSValue js_el_get_firstElementChild(JSContext* ctx, JSValue this_val) {
@@ -348,12 +532,8 @@ JSValue js_el_set_nodeValue(JSContext* ctx, JSValue this_val, JSValue val) {
348
532
 
349
533
  JSValue js_el_get_childNodes(JSContext* ctx, JSValue this_val) {
350
534
  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;
535
+ if (!node) return JS_NewArray(ctx);
536
+ return LiveCollectionBindings::makeChildNodes(ctx, node);
357
537
  }
358
538
 
359
539
  JSValue js_el_get_firstChild(JSContext* ctx, JSValue this_val) {
@@ -551,8 +731,7 @@ JSValue js_el_insertBefore(JSContext* ctx, JSValue this_val, int argc, JSValue*
551
731
  if (ref) {
552
732
  lxb_dom_node_t* receiver_node = unwrap_node(ctx, this_val);
553
733
  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;
734
+ return throw_dom_exception(ctx, "NotFoundError", "reference node is not a child of this node");
556
735
  }
557
736
  parent_node = receiver_node;
558
737
  inserted = expand_and_insert(new_node, [&](lxb_dom_node_t* n) {
@@ -619,8 +798,7 @@ JSValue js_el_replaceChild(JSContext* ctx, JSValue this_val, int argc, JSValue*
619
798
  auto* old_node = unwrap_node(ctx, argv[1]);
620
799
  if (!new_node || !old_node) return JS_NULL;
621
800
  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;
801
+ return throw_dom_exception(ctx, "NotFoundError", "the node to be replaced is not a child of this node");
624
802
  }
625
803
 
626
804
  lxb_dom_node_t* prev_sib = old_node->prev;
@@ -835,8 +1013,7 @@ JSValue js_el_insertAdjacentHTML(JSContext* ctx, JSValue this_val, int argc, JSV
835
1013
  next_sib = ref;
836
1014
  } else {
837
1015
  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;
1016
+ return throw_dom_exception(ctx, "SyntaxError", ("invalid insertAdjacentHTML position '" + pos + "'").c_str());
840
1017
  }
841
1018
 
842
1019
  auto* rctx = get_ctx(ctx);
@@ -881,9 +1058,9 @@ JSValue js_el_getElementsByClassName(JSContext* ctx, JSValue this_val, int argc,
881
1058
  if (!el || argc < 1) return JS_NewArray(ctx);
882
1059
  const char* names = JS_ToCString(ctx, argv[0]);
883
1060
  if (!names) return JS_NewArray(ctx);
884
- auto results = get_doc(ctx)->querySelectorAllFromEl(el, classNames_to_selector(names));
1061
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, el, classNames_to_selector(names));
885
1062
  JS_FreeCString(ctx, names);
886
- return make_element_array(ctx, results);
1063
+ return result;
887
1064
  }
888
1065
 
889
1066
  JSValue js_el_getElementsByTagName(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
@@ -891,9 +1068,9 @@ JSValue js_el_getElementsByTagName(JSContext* ctx, JSValue this_val, int argc, J
891
1068
  if (!el || argc < 1) return JS_NewArray(ctx);
892
1069
  const char* tag = JS_ToCString(ctx, argv[0]);
893
1070
  if (!tag) return JS_NewArray(ctx);
894
- auto results = get_doc(ctx)->querySelectorAllFromEl(el, tag);
1071
+ JSValue result = LiveCollectionBindings::makeBySelector(ctx, el, tag);
895
1072
  JS_FreeCString(ctx, tag);
896
- return make_element_array(ctx, results);
1073
+ return result;
897
1074
  }
898
1075
 
899
1076
  JSValue js_el_getAttributeNames(JSContext* ctx, JSValue this_val, int, JSValue*) {
@@ -966,6 +1143,17 @@ JSValue js_el_toggleAttribute(JSContext* ctx, JSValue this_val, int argc, JSValu
966
1143
  return JS_NewBool(ctx, force_present);
967
1144
  }
968
1145
 
1146
+ JSValue js_el_getBoundingClientRect(JSContext* ctx, JSValue this_val, int, JSValue*) {
1147
+ auto* el = unwrap_element(ctx, this_val);
1148
+ if (!el) return JS_NULL;
1149
+ JSValue rect = JS_NewObject(ctx);
1150
+ static const char* kFields[] = { "x", "y", "width", "height", "top", "right", "bottom", "left" };
1151
+ for (const char* field : kFields) {
1152
+ JS_SetPropertyStr(ctx, rect, field, JS_NewInt32(ctx, 0));
1153
+ }
1154
+ return rect;
1155
+ }
1156
+
969
1157
  JSValue js_el_isSameNode(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
970
1158
  auto* node = unwrap_node(ctx, this_val);
971
1159
  if (!node || argc < 1) return JS_FALSE;
@@ -1022,6 +1210,37 @@ JSValue js_el_isEqualNode(JSContext* ctx, JSValue this_val, int argc, JSValue* a
1022
1210
  return JS_NewBool(ctx, nodes_equal(node, other));
1023
1211
  }
1024
1212
 
1213
+ void collect_text_descendants(lxb_dom_node_t* node, std::vector<void*>& out) {
1214
+ for (lxb_dom_node_t* child = node->first_child; child; child = child->next) {
1215
+ if (child->type == LXB_DOM_NODE_TYPE_TEXT) out.push_back(child);
1216
+ else if (child->first_child) collect_text_descendants(child, out);
1217
+ }
1218
+ }
1219
+
1220
+ JSValue js_el_normalize(JSContext* ctx, JSValue this_val, int, JSValue*) {
1221
+ auto* node = unwrap_node(ctx, this_val);
1222
+ if (!node) return JS_UNDEFINED;
1223
+
1224
+ // normalize() only merges/drops Text nodes (leaves, no children of their
1225
+ // own), so a flat invalidate is enough — no need for the *_deep variant.
1226
+ std::vector<void*> text_nodes;
1227
+ collect_text_descendants(node, text_nodes);
1228
+ if (!text_nodes.empty()) {
1229
+ invalidate_node_cache_batch(ctx, get_ctx(ctx), text_nodes);
1230
+ }
1231
+
1232
+ get_doc(ctx)->normalize(node);
1233
+ return JS_UNDEFINED;
1234
+ }
1235
+
1236
+ JSValue js_el_compareDocumentPosition(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
1237
+ auto* node = unwrap_node(ctx, this_val);
1238
+ if (!node || argc < 1) return JS_NewInt32(ctx, 0);
1239
+ auto* other = unwrap_node(ctx, argv[0]);
1240
+ if (!other) return JS_NewInt32(ctx, 0);
1241
+ return JS_NewInt32(ctx, get_doc(ctx)->compareDocumentPosition(node, other));
1242
+ }
1243
+
1025
1244
  } // namespace
1026
1245
 
1027
1246
  void ElementBindings::install(JSContext* ctx) {
@@ -1048,6 +1267,8 @@ void ElementBindings::install(JSContext* ctx) {
1048
1267
  JS_SetPropertyStr(ctx, node_proto, "replaceWith", JS_NewCFunction(ctx, js_el_replaceWith, "replaceWith", 0));
1049
1268
  JS_SetPropertyStr(ctx, node_proto, "isSameNode", JS_NewCFunction(ctx, js_el_isSameNode, "isSameNode", 1));
1050
1269
  JS_SetPropertyStr(ctx, node_proto, "isEqualNode", JS_NewCFunction(ctx, js_el_isEqualNode, "isEqualNode", 1));
1270
+ JS_SetPropertyStr(ctx, node_proto, "normalize", JS_NewCFunction(ctx, js_el_normalize, "normalize", 0));
1271
+ JS_SetPropertyStr(ctx, node_proto, "compareDocumentPosition", JS_NewCFunction(ctx, js_el_compareDocumentPosition, "compareDocumentPosition", 1));
1051
1272
 
1052
1273
  define_prop(ctx, node_proto, "nodeType", js_el_get_nodeType, nullptr);
1053
1274
  define_prop(ctx, node_proto, "nodeName", js_el_get_nodeName, nullptr);
@@ -1083,10 +1304,13 @@ void ElementBindings::install(JSContext* ctx) {
1083
1304
  JS_SetPropertyStr(ctx, proto, "insertAdjacentHTML", JS_NewCFunction(ctx, js_el_insertAdjacentHTML, "insertAdjacentHTML", 2));
1084
1305
  JS_SetPropertyStr(ctx, proto, "append", JS_NewCFunction(ctx, js_el_append, "append", 0));
1085
1306
  JS_SetPropertyStr(ctx, proto, "prepend", JS_NewCFunction(ctx, js_el_prepend, "prepend", 0));
1307
+ JS_SetPropertyStr(ctx, proto, "getBoundingClientRect", JS_NewCFunction(ctx, js_el_getBoundingClientRect, "getBoundingClientRect", 0));
1086
1308
 
1087
1309
  define_prop(ctx, proto, "tagName", js_el_get_tagName, nullptr);
1088
1310
  define_prop(ctx, proto, "id", js_el_get_id, js_el_set_id);
1089
1311
  define_prop(ctx, proto, "className", js_el_get_className, js_el_set_className);
1312
+ define_prop(ctx, proto, "value", js_el_get_value, js_el_set_value);
1313
+ define_prop(ctx, proto, "checked", js_el_get_checked, js_el_set_checked);
1090
1314
  define_prop(ctx, proto, "innerHTML", js_el_get_innerHTML, js_el_set_innerHTML);
1091
1315
  define_prop(ctx, proto, "outerHTML", js_el_get_outerHTML, nullptr);
1092
1316
  define_prop(ctx, proto, "children", js_el_get_children, nullptr);
@@ -1101,6 +1325,18 @@ void ElementBindings::install(JSContext* ctx) {
1101
1325
  define_prop(ctx, proto, "attributes", js_el_get_attributes, nullptr);
1102
1326
 
1103
1327
  JS_SetClassProto(ctx, js_element_class_id, proto);
1328
+
1329
+ JSValue node_proto_ref = JS_GetClassProto(ctx, js_node_class_id);
1330
+ JSValue element_proto_ref = JS_GetClassProto(ctx, js_element_class_id);
1331
+
1332
+ JS_FreeValue(ctx, define_global_constructor(ctx, "Node", node_proto_ref));
1333
+ JSValue element_ctor = define_global_constructor(ctx, "Element", element_proto_ref);
1334
+ JSValue global = JS_GetGlobalObject(ctx);
1335
+ JS_SetPropertyStr(ctx, global, "HTMLElement", element_ctor);
1336
+ JS_FreeValue(ctx, global);
1337
+
1338
+ JS_FreeValue(ctx, node_proto_ref);
1339
+ JS_FreeValue(ctx, element_proto_ref);
1104
1340
  }
1105
1341
 
1106
1342
  } // namespace margelo::nitro::nitrojsdom