@salve-software/react-native-nitro-jsdom 1.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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/NitroJsdom.podspec +63 -0
  3. package/README.md +95 -0
  4. package/android/CMakeLists.txt +86 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/nitrojsdom/NitroJsdomPackage.kt +20 -0
  11. package/cpp/HybridHtmlSandbox.cpp +171 -0
  12. package/cpp/HybridHtmlSandbox.hpp +47 -0
  13. package/cpp/lexbor/LexborDocument.cpp +365 -0
  14. package/cpp/lexbor/LexborDocument.hpp +71 -0
  15. package/cpp/quickjs/DOMBindings.cpp +57 -0
  16. package/cpp/quickjs/DOMBindings.hpp +21 -0
  17. package/cpp/quickjs/DOMBindingsInternal.cpp +157 -0
  18. package/cpp/quickjs/DOMBindingsInternal.hpp +83 -0
  19. package/cpp/quickjs/MutationObservers.cpp +546 -0
  20. package/cpp/quickjs/MutationObservers.hpp +127 -0
  21. package/cpp/quickjs/QuickJSRuntime.cpp +475 -0
  22. package/cpp/quickjs/QuickJSRuntime.hpp +110 -0
  23. package/cpp/quickjs/Storage.cpp +130 -0
  24. package/cpp/quickjs/Storage.hpp +40 -0
  25. package/cpp/quickjs/bindings/ClassListBindings.cpp +241 -0
  26. package/cpp/quickjs/bindings/ClassListBindings.hpp +29 -0
  27. package/cpp/quickjs/bindings/DatasetBindings.cpp +120 -0
  28. package/cpp/quickjs/bindings/DatasetBindings.hpp +20 -0
  29. package/cpp/quickjs/bindings/DocumentBindings.cpp +155 -0
  30. package/cpp/quickjs/bindings/DocumentBindings.hpp +19 -0
  31. package/cpp/quickjs/bindings/ElementBindings.cpp +1106 -0
  32. package/cpp/quickjs/bindings/ElementBindings.hpp +19 -0
  33. package/cpp/quickjs/bindings/EventBindings.cpp +352 -0
  34. package/cpp/quickjs/bindings/EventBindings.hpp +21 -0
  35. package/cpp/quickjs/bindings/FetchBindings.cpp +347 -0
  36. package/cpp/quickjs/bindings/FetchBindings.hpp +16 -0
  37. package/cpp/quickjs/bindings/StyleBindings.cpp +245 -0
  38. package/cpp/quickjs/bindings/StyleBindings.hpp +22 -0
  39. package/cpp/quickjs/bindings/TimerBindings.cpp +94 -0
  40. package/cpp/quickjs/bindings/TimerBindings.hpp +16 -0
  41. package/cpp/quickjs/bindings/WindowBindings.cpp +211 -0
  42. package/cpp/quickjs/bindings/WindowBindings.hpp +16 -0
  43. package/ios/Bridge.h +8 -0
  44. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +138 -0
  45. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -0
  46. package/lib/commonjs/classes/JSDOM/index.js +17 -0
  47. package/lib/commonjs/classes/JSDOM/index.js.map +1 -0
  48. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js +2 -0
  49. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  50. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js +6 -0
  51. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  52. package/lib/commonjs/classes/JSDOM/types/index.js +2 -0
  53. package/lib/commonjs/classes/JSDOM/types/index.js.map +1 -0
  54. package/lib/commonjs/classes/index.js +17 -0
  55. package/lib/commonjs/classes/index.js.map +1 -0
  56. package/lib/commonjs/index.js +17 -0
  57. package/lib/commonjs/index.js.map +1 -0
  58. package/lib/commonjs/package.json +1 -0
  59. package/lib/commonjs/specs/HtmlSandbox.nitro.js +6 -0
  60. package/lib/commonjs/specs/HtmlSandbox.nitro.js.map +1 -0
  61. package/lib/module/classes/JSDOM/JSDOM.class.js +133 -0
  62. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -0
  63. package/lib/module/classes/JSDOM/index.js +4 -0
  64. package/lib/module/classes/JSDOM/index.js.map +1 -0
  65. package/lib/module/classes/JSDOM/types/IFetchOptions.js +2 -0
  66. package/lib/module/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  67. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js +4 -0
  68. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  69. package/lib/module/classes/JSDOM/types/index.js +2 -0
  70. package/lib/module/classes/JSDOM/types/index.js.map +1 -0
  71. package/lib/module/classes/index.js +4 -0
  72. package/lib/module/classes/index.js.map +1 -0
  73. package/lib/module/index.js +4 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/specs/HtmlSandbox.nitro.js +4 -0
  76. package/lib/module/specs/HtmlSandbox.nitro.js.map +1 -0
  77. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts +74 -0
  78. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -0
  79. package/lib/typescript/src/classes/JSDOM/index.d.ts +3 -0
  80. package/lib/typescript/src/classes/JSDOM/index.d.ts.map +1 -0
  81. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts +14 -0
  82. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts.map +1 -0
  83. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +57 -0
  84. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -0
  85. package/lib/typescript/src/classes/JSDOM/types/index.d.ts +3 -0
  86. package/lib/typescript/src/classes/JSDOM/types/index.d.ts.map +1 -0
  87. package/lib/typescript/src/classes/index.d.ts +2 -0
  88. package/lib/typescript/src/classes/index.d.ts.map +1 -0
  89. package/lib/typescript/src/index.d.ts +2 -0
  90. package/lib/typescript/src/index.d.ts.map +1 -0
  91. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +13 -0
  92. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -0
  93. package/nitro.json +26 -0
  94. package/nitrogen/generated/.gitattributes +1 -0
  95. package/nitrogen/generated/android/NitroJsdom+autolinking.cmake +81 -0
  96. package/nitrogen/generated/android/NitroJsdom+autolinking.gradle +27 -0
  97. package/nitrogen/generated/android/NitroJsdomOnLoad.cpp +49 -0
  98. package/nitrogen/generated/android/NitroJsdomOnLoad.hpp +34 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrojsdom/NitroJsdomOnLoad.kt +35 -0
  100. package/nitrogen/generated/ios/NitroJsdom+autolinking.rb +62 -0
  101. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.cpp +17 -0
  102. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.hpp +27 -0
  103. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Umbrella.hpp +38 -0
  104. package/nitrogen/generated/ios/NitroJsdomAutolinking.mm +35 -0
  105. package/nitrogen/generated/ios/NitroJsdomAutolinking.swift +16 -0
  106. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.cpp +26 -0
  107. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +73 -0
  108. package/package.json +131 -0
  109. package/src/classes/JSDOM/JSDOM.class.ts +145 -0
  110. package/src/classes/JSDOM/index.ts +2 -0
  111. package/src/classes/JSDOM/types/IFetchOptions.ts +14 -0
  112. package/src/classes/JSDOM/types/IJSDOMOptions.ts +57 -0
  113. package/src/classes/JSDOM/types/index.ts +2 -0
  114. package/src/classes/index.ts +1 -0
  115. package/src/index.ts +1 -0
  116. package/src/specs/HtmlSandbox.nitro.ts +33 -0
@@ -0,0 +1,130 @@
1
+ #include "Storage.hpp"
2
+ #include "quickjs.h"
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ // ── Storage ───────────────────────────────────────────────────────────────────
7
+
8
+ std::optional<std::string> Storage::getItem(const std::string& key) const {
9
+ auto it = _data.find(key);
10
+ if (it == _data.end()) return std::nullopt;
11
+ return it->second;
12
+ }
13
+
14
+ void Storage::setItem(const std::string& key, const std::string& value) {
15
+ auto it = _data.find(key);
16
+ if (it == _data.end()) {
17
+ _order.push_back(key);
18
+ }
19
+ _data[key] = value;
20
+ }
21
+
22
+ void Storage::removeItem(const std::string& key) {
23
+ auto it = _data.find(key);
24
+ if (it == _data.end()) return;
25
+ _data.erase(it);
26
+ for (auto orderIt = _order.begin(); orderIt != _order.end(); ++orderIt) {
27
+ if (*orderIt == key) {
28
+ _order.erase(orderIt);
29
+ break;
30
+ }
31
+ }
32
+ }
33
+
34
+ void Storage::clear() {
35
+ _data.clear();
36
+ _order.clear();
37
+ }
38
+
39
+ std::optional<std::string> Storage::key(size_t index) const {
40
+ if (index >= _order.size()) return std::nullopt;
41
+ return _order[index];
42
+ }
43
+
44
+ // ── JS bindings ───────────────────────────────────────────────────────────────
45
+
46
+ static JSClassID js_storage_class_id = 0;
47
+ static JSClassDef js_storage_class = { "Storage", .finalizer = nullptr };
48
+
49
+ static Storage* unwrap_storage(JSValue this_val) {
50
+ return static_cast<Storage*>(JS_GetOpaque(this_val, js_storage_class_id));
51
+ }
52
+
53
+ static std::string js_to_std_string(JSContext* ctx, JSValue v) {
54
+ JSValue str_val = JS_ToString(ctx, v);
55
+ std::string out;
56
+ const char* s = JS_ToCString(ctx, str_val);
57
+ if (s) { out = s; JS_FreeCString(ctx, s); }
58
+ JS_FreeValue(ctx, str_val);
59
+ return out;
60
+ }
61
+
62
+ static JSValue js_storage_getItem(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
63
+ Storage* storage = unwrap_storage(this_val);
64
+ if (!storage || argc < 1) return JS_NULL;
65
+ auto value = storage->getItem(js_to_std_string(ctx, argv[0]));
66
+ if (!value.has_value()) return JS_NULL;
67
+ return JS_NewStringLen(ctx, value->c_str(), value->size());
68
+ }
69
+
70
+ static JSValue js_storage_setItem(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
71
+ Storage* storage = unwrap_storage(this_val);
72
+ if (!storage || argc < 2) return JS_UNDEFINED;
73
+ storage->setItem(js_to_std_string(ctx, argv[0]), js_to_std_string(ctx, argv[1]));
74
+ return JS_UNDEFINED;
75
+ }
76
+
77
+ static JSValue js_storage_removeItem(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
78
+ Storage* storage = unwrap_storage(this_val);
79
+ if (!storage || argc < 1) return JS_UNDEFINED;
80
+ storage->removeItem(js_to_std_string(ctx, argv[0]));
81
+ return JS_UNDEFINED;
82
+ }
83
+
84
+ static JSValue js_storage_clear(JSContext* ctx, JSValue this_val, int, JSValue*) {
85
+ Storage* storage = unwrap_storage(this_val);
86
+ if (storage) storage->clear();
87
+ return JS_UNDEFINED;
88
+ }
89
+
90
+ static JSValue js_storage_key(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
91
+ Storage* storage = unwrap_storage(this_val);
92
+ if (!storage || argc < 1) return JS_NULL;
93
+ int32_t index = 0;
94
+ if (JS_ToInt32(ctx, &index, argv[0]) < 0 || index < 0) return JS_NULL;
95
+ auto k = storage->key(static_cast<size_t>(index));
96
+ if (!k.has_value()) return JS_NULL;
97
+ return JS_NewStringLen(ctx, k->c_str(), k->size());
98
+ }
99
+
100
+ static JSValue js_storage_get_length(JSContext* ctx, JSValue this_val) {
101
+ Storage* storage = unwrap_storage(this_val);
102
+ return JS_NewInt32(ctx, storage ? static_cast<int32_t>(storage->length()) : 0);
103
+ }
104
+
105
+ void installStorage(JSContext* ctx, const char* globalName, Storage* storage) {
106
+ if (js_storage_class_id == 0) {
107
+ JS_NewClassID(&js_storage_class_id);
108
+ }
109
+ JS_NewClass(JS_GetRuntime(ctx), js_storage_class_id, &js_storage_class);
110
+
111
+ JSValue obj = JS_NewObjectClass(ctx, js_storage_class_id);
112
+ JS_SetOpaque(obj, storage);
113
+
114
+ JS_SetPropertyStr(ctx, obj, "getItem", JS_NewCFunction(ctx, js_storage_getItem, "getItem", 1));
115
+ JS_SetPropertyStr(ctx, obj, "setItem", JS_NewCFunction(ctx, js_storage_setItem, "setItem", 2));
116
+ JS_SetPropertyStr(ctx, obj, "removeItem", JS_NewCFunction(ctx, js_storage_removeItem, "removeItem", 1));
117
+ JS_SetPropertyStr(ctx, obj, "clear", JS_NewCFunction(ctx, js_storage_clear, "clear", 0));
118
+ JS_SetPropertyStr(ctx, obj, "key", JS_NewCFunction(ctx, js_storage_key, "key", 1));
119
+
120
+ JSAtom atom = JS_NewAtom(ctx, "length");
121
+ JSValue getter = JS_NewCFunction2(ctx, (JSCFunction*)js_storage_get_length, "length", 0, JS_CFUNC_getter, 0);
122
+ JS_DefinePropertyGetSet(ctx, obj, atom, getter, JS_UNDEFINED, JS_PROP_CONFIGURABLE);
123
+ JS_FreeAtom(ctx, atom);
124
+
125
+ JSValue global = JS_GetGlobalObject(ctx);
126
+ JS_SetPropertyStr(ctx, global, globalName, obj);
127
+ JS_FreeValue(ctx, global);
128
+ }
129
+
130
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,40 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <vector>
5
+ #include <unordered_map>
6
+ #include <optional>
7
+ #include <cstddef>
8
+
9
+ // Forward-declare the opaque JSContext/JSValue types to avoid including quickjs.h here
10
+ typedef struct JSContext JSContext;
11
+
12
+ namespace margelo::nitro::nitrojsdom {
13
+
14
+ // ── Storage ───────────────────────────────────────────────────────────────────
15
+ //
16
+ // In-memory backing store for `localStorage` / `sessionStorage`. Preserves
17
+ // insertion order (per the Web Storage spec's `key(index)` iteration order);
18
+ // re-setting an existing key updates its value in place without moving it.
19
+ // Purely in-memory — not persisted across JSDOM instances or app restarts.
20
+
21
+ class Storage {
22
+ public:
23
+ std::optional<std::string> getItem(const std::string& key) const;
24
+ void setItem(const std::string& key, const std::string& value);
25
+ void removeItem(const std::string& key);
26
+ void clear();
27
+ std::optional<std::string> key(size_t index) const;
28
+ size_t length() const { return _order.size(); }
29
+
30
+ private:
31
+ std::vector<std::string> _order;
32
+ std::unordered_map<std::string, std::string> _data;
33
+ };
34
+
35
+ // Registers `getItem`/`setItem`/`removeItem`/`clear`/`key`/`length` on a new JS
36
+ // object bound to `globalName` on the given context, backed by `storage`.
37
+ // `storage` must outlive the QuickJS context (owned by RuntimeContext).
38
+ void installStorage(JSContext* ctx, const char* globalName, Storage* storage);
39
+
40
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,241 @@
1
+ #include "ClassListBindings.hpp"
2
+ #include "../DOMBindingsInternal.hpp"
3
+ #include "../QuickJSRuntime.hpp"
4
+ #include "../MutationObservers.hpp"
5
+ #include <lexbor/dom/dom.h>
6
+ #include <algorithm>
7
+ #include <optional>
8
+ #include <sstream>
9
+ #include <vector>
10
+
11
+ namespace margelo::nitro::nitrojsdom {
12
+
13
+ namespace {
14
+
15
+ JSClassID js_classList_class_id = 0;
16
+ JSClassDef js_classList_class = { "DOMTokenList", .finalizer = nullptr };
17
+
18
+ lxb_dom_element_t* unwrap_classList(JSContext* ctx, JSValue val) {
19
+ return static_cast<lxb_dom_element_t*>(JS_GetOpaque(val, js_classList_class_id));
20
+ }
21
+
22
+ std::vector<std::string> split_classes(const std::string& str) {
23
+ std::vector<std::string> result;
24
+ std::istringstream iss(str);
25
+ std::string token;
26
+ while (iss >> token) result.push_back(token);
27
+ return result;
28
+ }
29
+
30
+ std::string join_classes(const std::vector<std::string>& v) {
31
+ std::string r;
32
+ for (size_t i = 0; i < v.size(); i++) { if (i) r += ' '; r += v[i]; }
33
+ return r;
34
+ }
35
+
36
+ // Validates a DOMTokenList token: throws on empty or whitespace-containing token.
37
+ bool validate_token(JSContext* ctx, const char* token) {
38
+ if (!token || token[0] == '\0') {
39
+ JS_ThrowTypeError(ctx, "The token provided must not be empty.");
40
+ return false;
41
+ }
42
+ for (const char* p = token; *p; p++) {
43
+ unsigned char c = (unsigned char)*p;
44
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f') {
45
+ JS_ThrowTypeError(ctx, "The token provided contains HTML space characters, which are not valid in tokens.");
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+
52
+ JSValue js_classList_add(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
53
+ auto* el = unwrap_classList(ctx, this_val);
54
+ if (!el) return JS_UNDEFINED;
55
+
56
+ auto* rctx = get_ctx(ctx);
57
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
58
+ std::optional<std::string> old_val;
59
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver())
60
+ old_val = get_class_attr(el);
61
+
62
+ // Validate all tokens before mutating (spec: validate each, then add all)
63
+ std::vector<std::string> tokens;
64
+ for (int i = 0; i < argc; i++) {
65
+ const char* cls = JS_ToCString(ctx, argv[i]);
66
+ if (!cls) return JS_EXCEPTION;
67
+ if (!validate_token(ctx, cls)) { JS_FreeCString(ctx, cls); return JS_EXCEPTION; }
68
+ tokens.emplace_back(cls);
69
+ JS_FreeCString(ctx, cls);
70
+ }
71
+
72
+ auto classes = split_classes(get_class_attr(el));
73
+ for (auto& tok : tokens) {
74
+ if (std::find(classes.begin(), classes.end(), tok) == classes.end())
75
+ classes.push_back(tok);
76
+ }
77
+ set_class_attr(el, join_classes(classes));
78
+
79
+ if (has_obs) {
80
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el),
81
+ "class", old_val);
82
+ }
83
+ return JS_UNDEFINED;
84
+ }
85
+
86
+ JSValue js_classList_remove(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
87
+ auto* el = unwrap_classList(ctx, this_val);
88
+ if (!el) return JS_UNDEFINED;
89
+
90
+ auto* rctx = get_ctx(ctx);
91
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
92
+ std::optional<std::string> old_val;
93
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver())
94
+ old_val = get_class_attr(el);
95
+
96
+ // Validate all tokens before mutating
97
+ std::vector<std::string> tokens;
98
+ for (int i = 0; i < argc; i++) {
99
+ const char* cls = JS_ToCString(ctx, argv[i]);
100
+ if (!cls) return JS_EXCEPTION;
101
+ if (!validate_token(ctx, cls)) { JS_FreeCString(ctx, cls); return JS_EXCEPTION; }
102
+ tokens.emplace_back(cls);
103
+ JS_FreeCString(ctx, cls);
104
+ }
105
+
106
+ auto classes = split_classes(get_class_attr(el));
107
+ for (auto& tok : tokens)
108
+ classes.erase(std::remove(classes.begin(), classes.end(), tok), classes.end());
109
+ set_class_attr(el, join_classes(classes));
110
+
111
+ if (has_obs) {
112
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el),
113
+ "class", old_val);
114
+ }
115
+ return JS_UNDEFINED;
116
+ }
117
+
118
+ JSValue js_classList_contains(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
119
+ auto* el = unwrap_classList(ctx, this_val);
120
+ if (!el || argc < 1) return JS_FALSE;
121
+ const char* cls = JS_ToCString(ctx, argv[0]);
122
+ if (!cls) return JS_EXCEPTION;
123
+ if (!validate_token(ctx, cls)) { JS_FreeCString(ctx, cls); return JS_EXCEPTION; }
124
+ auto classes = split_classes(get_class_attr(el));
125
+ bool found = std::find(classes.begin(), classes.end(), cls) != classes.end();
126
+ JS_FreeCString(ctx, cls);
127
+ return JS_NewBool(ctx, found);
128
+ }
129
+
130
+ JSValue js_classList_toggle(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
131
+ auto* el = unwrap_classList(ctx, this_val);
132
+ if (!el || argc < 1) return JS_FALSE;
133
+ const char* cls = JS_ToCString(ctx, argv[0]);
134
+ if (!cls) return JS_EXCEPTION;
135
+ if (!validate_token(ctx, cls)) { JS_FreeCString(ctx, cls); return JS_EXCEPTION; }
136
+
137
+ auto* rctx = get_ctx(ctx);
138
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
139
+ std::optional<std::string> old_val;
140
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver())
141
+ old_val = get_class_attr(el);
142
+
143
+ auto classes = split_classes(get_class_attr(el));
144
+ auto it = std::find(classes.begin(), classes.end(), cls);
145
+
146
+ bool added;
147
+ bool force_present = argc >= 2 && !JS_IsUndefined(argv[1]);
148
+ if (force_present) {
149
+ bool force = JS_ToBool(ctx, argv[1]) > 0;
150
+ if (force) {
151
+ if (it == classes.end()) classes.push_back(cls);
152
+ added = true;
153
+ } else {
154
+ if (it != classes.end()) classes.erase(it);
155
+ added = false;
156
+ }
157
+ } else {
158
+ if (it != classes.end()) { classes.erase(it); added = false; }
159
+ else { classes.push_back(cls); added = true; }
160
+ }
161
+
162
+ JS_FreeCString(ctx, cls);
163
+ set_class_attr(el, join_classes(classes));
164
+
165
+ if (has_obs) {
166
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el),
167
+ "class", old_val);
168
+ }
169
+ return JS_NewBool(ctx, added);
170
+ }
171
+
172
+ JSValue js_classList_replace(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
173
+ auto* el = unwrap_classList(ctx, this_val);
174
+ if (!el || argc < 2) return JS_FALSE;
175
+ const char* oldCls = JS_ToCString(ctx, argv[0]);
176
+ if (!oldCls) return JS_EXCEPTION;
177
+ if (!validate_token(ctx, oldCls)) { JS_FreeCString(ctx, oldCls); return JS_EXCEPTION; }
178
+ const char* newCls = JS_ToCString(ctx, argv[1]);
179
+ if (!newCls) { JS_FreeCString(ctx, oldCls); return JS_EXCEPTION; }
180
+ if (!validate_token(ctx, newCls)) { JS_FreeCString(ctx, oldCls); JS_FreeCString(ctx, newCls); return JS_EXCEPTION; }
181
+
182
+ auto* rctx = get_ctx(ctx);
183
+ bool has_obs = rctx && rctx->mutation_observers && !rctx->mutation_observers->empty();
184
+ std::optional<std::string> old_val;
185
+ if (has_obs && rctx->mutation_observers->hasAttributeOldValueObserver())
186
+ old_val = get_class_attr(el);
187
+
188
+ auto classes = split_classes(get_class_attr(el));
189
+ auto it = std::find(classes.begin(), classes.end(), oldCls);
190
+ bool replaced = false;
191
+ if (it != classes.end()) {
192
+ *it = newCls;
193
+ // Remove any other occurrences of newCls to keep the set unique
194
+ classes.erase(std::remove(it + 1, classes.end(), newCls), classes.end());
195
+ replaced = true;
196
+ set_class_attr(el, join_classes(classes));
197
+ if (has_obs) {
198
+ rctx->mutation_observers->notifyAttribute(ctx, lxb_dom_interface_node(el),
199
+ "class", old_val);
200
+ }
201
+ }
202
+
203
+ JS_FreeCString(ctx, oldCls);
204
+ JS_FreeCString(ctx, newCls);
205
+ return JS_NewBool(ctx, replaced);
206
+ }
207
+
208
+ } // namespace
209
+
210
+ std::string get_class_attr(lxb_dom_element_t* el) {
211
+ size_t len = 0;
212
+ const lxb_char_t* val = lxb_dom_element_get_attribute(el,
213
+ reinterpret_cast<const lxb_char_t*>("class"), 5, &len);
214
+ return val ? std::string(reinterpret_cast<const char*>(val), len) : "";
215
+ }
216
+
217
+ void set_class_attr(lxb_dom_element_t* el, const std::string& classes) {
218
+ // Always write the attribute (even when empty) so classList.value serializes
219
+ // back to "" rather than the attribute disappearing after the last token is removed.
220
+ lxb_dom_element_set_attribute(el,
221
+ reinterpret_cast<const lxb_char_t*>("class"), 5,
222
+ reinterpret_cast<const lxb_char_t*>(classes.data()), classes.size());
223
+ }
224
+
225
+ void ClassListBindings::install(JSContext* ctx) {
226
+ if (js_classList_class_id == 0) JS_NewClassID(&js_classList_class_id);
227
+ JS_NewClass(JS_GetRuntime(ctx), js_classList_class_id, &js_classList_class);
228
+ }
229
+
230
+ JSValue ClassListBindings::make(JSContext* ctx, lxb_dom_element_t* el) {
231
+ JSValue obj = JS_NewObjectClass(ctx, js_classList_class_id);
232
+ JS_SetOpaque(obj, el);
233
+ JS_SetPropertyStr(ctx, obj, "add", JS_NewCFunction(ctx, js_classList_add, "add", 1));
234
+ JS_SetPropertyStr(ctx, obj, "remove", JS_NewCFunction(ctx, js_classList_remove, "remove", 1));
235
+ JS_SetPropertyStr(ctx, obj, "contains", JS_NewCFunction(ctx, js_classList_contains, "contains", 1));
236
+ JS_SetPropertyStr(ctx, obj, "toggle", JS_NewCFunction(ctx, js_classList_toggle, "toggle", 1));
237
+ JS_SetPropertyStr(ctx, obj, "replace", JS_NewCFunction(ctx, js_classList_replace, "replace", 2));
238
+ return obj;
239
+ }
240
+
241
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,29 @@
1
+ #pragma once
2
+
3
+ // element.classList (DOMTokenList) — registered as its own QuickJS class since
4
+ // it's a distinct object handed out by the "classList" accessor, not a set of
5
+ // methods on Element itself.
6
+
7
+ #include "quickjs.h"
8
+ #include <string>
9
+
10
+ struct lxb_dom_element;
11
+ typedef struct lxb_dom_element lxb_dom_element_t;
12
+
13
+ namespace margelo::nitro::nitrojsdom {
14
+
15
+ class ClassListBindings {
16
+ public:
17
+ // Registers the DOMTokenList class. Must run before make() is called.
18
+ static void install(JSContext* ctx);
19
+
20
+ // Wraps `el` as a classList instance (add/remove/contains/toggle/replace).
21
+ static JSValue make(JSContext* ctx, lxb_dom_element_t* el);
22
+ };
23
+
24
+ // Shared with ElementBindings (className getter/setter) and StyleBindings-adjacent
25
+ // callers that need the raw `class` attribute value.
26
+ std::string get_class_attr(lxb_dom_element_t* el);
27
+ void set_class_attr(lxb_dom_element_t* el, const std::string& classes);
28
+
29
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,120 @@
1
+ #include "DatasetBindings.hpp"
2
+ #include "../DOMBindingsInternal.hpp"
3
+ #include <lexbor/dom/dom.h>
4
+ #include <vector>
5
+
6
+ namespace margelo::nitro::nitrojsdom {
7
+
8
+ namespace {
9
+
10
+ JSClassID js_dataset_class_id = 0;
11
+
12
+ lxb_dom_element_t* unwrap_dataset(JSContext* ctx, JSValue val) {
13
+ return static_cast<lxb_dom_element_t*>(JS_GetOpaque(val, js_dataset_class_id));
14
+ }
15
+
16
+ std::string dataset_prop_to_attr(JSContext* ctx, JSAtom prop) {
17
+ JSValue key_val = JS_AtomToString(ctx, prop);
18
+ const char* key = JS_ToCString(ctx, key_val);
19
+ JS_FreeValue(ctx, key_val);
20
+ std::string attr_name = key ? ("data-" + camel_to_attr_suffix(key)) : "";
21
+ if (key) JS_FreeCString(ctx, key);
22
+ return attr_name;
23
+ }
24
+
25
+ int js_dataset_get_own_property(JSContext* ctx, JSPropertyDescriptor* desc, JSValue obj, JSAtom prop) {
26
+ auto* el = unwrap_dataset(ctx, obj);
27
+ if (!el) return 0;
28
+ std::string attr_name = dataset_prop_to_attr(ctx, prop);
29
+ if (attr_name.empty()) return 0;
30
+
31
+ size_t len = 0;
32
+ const lxb_char_t* val = lxb_dom_element_get_attribute(el,
33
+ reinterpret_cast<const lxb_char_t*>(attr_name.data()), attr_name.size(), &len);
34
+ if (!val) return 0;
35
+
36
+ if (desc) {
37
+ desc->flags = JS_PROP_ENUMERABLE | JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE;
38
+ desc->value = JS_NewStringLen(ctx, reinterpret_cast<const char*>(val), len);
39
+ desc->getter = JS_UNDEFINED;
40
+ desc->setter = JS_UNDEFINED;
41
+ }
42
+ return 1;
43
+ }
44
+
45
+ int js_dataset_define_own_property(JSContext* ctx, JSValue this_obj, JSAtom prop, JSValue val,
46
+ JSValue, JSValue, int) {
47
+ auto* el = unwrap_dataset(ctx, this_obj);
48
+ if (!el) return 0;
49
+ std::string attr_name = dataset_prop_to_attr(ctx, prop);
50
+ if (attr_name.empty()) return 0;
51
+
52
+ const char* str = JS_ToCString(ctx, val);
53
+ if (str) {
54
+ lxb_dom_element_set_attribute(el,
55
+ reinterpret_cast<const lxb_char_t*>(attr_name.data()), attr_name.size(),
56
+ reinterpret_cast<const lxb_char_t*>(str), strlen(str));
57
+ JS_FreeCString(ctx, str);
58
+ }
59
+ return 1;
60
+ }
61
+
62
+ int js_dataset_delete_property(JSContext* ctx, JSValue obj, JSAtom prop) {
63
+ auto* el = unwrap_dataset(ctx, obj);
64
+ if (!el) return 1;
65
+ std::string attr_name = dataset_prop_to_attr(ctx, prop);
66
+ if (!attr_name.empty()) {
67
+ lxb_dom_element_remove_attribute(el,
68
+ reinterpret_cast<const lxb_char_t*>(attr_name.data()), attr_name.size());
69
+ }
70
+ return 1;
71
+ }
72
+
73
+ int js_dataset_get_own_property_names(JSContext* ctx, JSPropertyEnum** ptab, uint32_t* plen, JSValue obj) {
74
+ *ptab = nullptr;
75
+ *plen = 0;
76
+ auto* el = unwrap_dataset(ctx, obj);
77
+ if (!el) return 0;
78
+
79
+ std::vector<std::string> keys;
80
+ for (lxb_dom_attr_t* attr = lxb_dom_element_first_attribute(el); attr; attr = lxb_dom_element_next_attribute(attr)) {
81
+ size_t len = 0;
82
+ const lxb_char_t* name = lxb_dom_attr_qualified_name(attr, &len);
83
+ std::string n(reinterpret_cast<const char*>(name), len);
84
+ if (n.rfind("data-", 0) == 0) keys.push_back(attr_suffix_to_camel(n.substr(5)));
85
+ }
86
+
87
+ auto* tab = static_cast<JSPropertyEnum*>(js_malloc(ctx, sizeof(JSPropertyEnum) * (keys.empty() ? 1 : keys.size())));
88
+ if (!tab) return -1;
89
+ for (size_t i = 0; i < keys.size(); i++) {
90
+ tab[i].is_enumerable = 1;
91
+ tab[i].atom = JS_NewAtom(ctx, keys[i].c_str());
92
+ }
93
+ *ptab = tab;
94
+ *plen = (uint32_t)keys.size();
95
+ return 0;
96
+ }
97
+
98
+ JSClassExoticMethods js_dataset_exotic = {
99
+ .get_own_property = js_dataset_get_own_property,
100
+ .get_own_property_names = js_dataset_get_own_property_names,
101
+ .delete_property = js_dataset_delete_property,
102
+ .define_own_property = js_dataset_define_own_property,
103
+ };
104
+
105
+ JSClassDef js_dataset_class = { "DOMStringMap", .finalizer = nullptr, .exotic = &js_dataset_exotic };
106
+
107
+ } // namespace
108
+
109
+ void DatasetBindings::install(JSContext* ctx) {
110
+ JS_NewClassID(&js_dataset_class_id);
111
+ JS_NewClass(JS_GetRuntime(ctx), js_dataset_class_id, &js_dataset_class);
112
+ }
113
+
114
+ JSValue DatasetBindings::make(JSContext* ctx, lxb_dom_element_t* el) {
115
+ JSValue obj = JS_NewObjectClass(ctx, js_dataset_class_id);
116
+ JS_SetOpaque(obj, el);
117
+ return obj;
118
+ }
119
+
120
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,20 @@
1
+ #pragma once
2
+
3
+ // element.dataset (DOMStringMap), mirroring data-* attributes. Implemented as a
4
+ // QuickJS exotic-property class so `el.dataset.foo = 'x'` writes straight
5
+ // through to the `data-foo` attribute, rather than a one-shot snapshot object.
6
+
7
+ #include "quickjs.h"
8
+
9
+ struct lxb_dom_element;
10
+ typedef struct lxb_dom_element lxb_dom_element_t;
11
+
12
+ namespace margelo::nitro::nitrojsdom {
13
+
14
+ class DatasetBindings {
15
+ public:
16
+ static void install(JSContext* ctx);
17
+ static JSValue make(JSContext* ctx, lxb_dom_element_t* el);
18
+ };
19
+
20
+ } // namespace margelo::nitro::nitrojsdom