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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/android/CMakeLists.txt +14 -0
  2. package/cpp/HybridHtmlSandbox.cpp +16 -3
  3. package/cpp/HybridHtmlSandbox.hpp +1 -1
  4. package/cpp/lexbor/LexborDocument.cpp +217 -67
  5. package/cpp/lexbor/LexborDocument.hpp +29 -10
  6. package/cpp/quickjs/DOMBindings.cpp +28 -0
  7. package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
  8. package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
  9. package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
  10. package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
  11. package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
  12. package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
  13. package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
  14. package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
  15. package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
  16. package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
  17. package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
  18. package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
  19. package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
  20. package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
  21. package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
  22. package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
  23. package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
  24. package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
  25. package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
  26. package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
  27. package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
  28. package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
  29. package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
  30. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
  31. package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
  32. package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
  33. package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
  34. package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
  35. package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
  36. package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
  37. package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
  38. package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
  39. package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
  40. package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
  41. package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
  42. package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
  43. package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
  44. package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
  45. package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
  46. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
  47. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
  48. package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
  49. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
  50. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
  51. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
  52. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
  53. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
  54. package/package.json +1 -1
  55. package/src/classes/JSDOM/JSDOM.class.ts +1 -0
  56. package/src/specs/HtmlSandbox.nitro.ts +1 -1
@@ -0,0 +1,171 @@
1
+ #include "ShadowRootBindings.hpp"
2
+ #include "DOMExceptionBindings.hpp"
3
+ #include "../DOMBindingsInternal.hpp"
4
+ #include "../QuickJSRuntime.hpp"
5
+ #include "../../lexbor/LexborDocument.hpp"
6
+ #include <lexbor/html/html.h>
7
+ #include <lexbor/dom/dom.h>
8
+ #include <cstring>
9
+
10
+ namespace margelo::nitro::nitrojsdom {
11
+
12
+ namespace {
13
+
14
+ JSClassDef js_shadow_root_class = { "ShadowRoot", .finalizer = nullptr };
15
+
16
+ lxb_dom_shadow_root_t* unwrap_shadow_root(JSValue val) {
17
+ return static_cast<lxb_dom_shadow_root_t*>(JS_GetOpaque(val, js_shadow_root_class_id));
18
+ }
19
+
20
+ JSValue make_shadow_root(JSContext* ctx, void* shadow_ptr) {
21
+ if (!shadow_ptr) return JS_NULL;
22
+
23
+ RuntimeContext* rctx = get_ctx(ctx);
24
+ if (rctx) {
25
+ auto it = rctx->node_wrapper_cache.find(shadow_ptr);
26
+ if (it != rctx->node_wrapper_cache.end()) {
27
+ return JS_DupValue(ctx, *static_cast<JSValue*>(it->second));
28
+ }
29
+ }
30
+
31
+ JSValue obj = JS_NewObjectClass(ctx, js_shadow_root_class_id);
32
+ JS_SetOpaque(obj, shadow_ptr);
33
+ if (rctx) rctx->node_wrapper_cache[shadow_ptr] = new JSValue(JS_DupValue(ctx, obj));
34
+ return obj;
35
+ }
36
+
37
+ JSValue js_shadow_get_host(JSContext* ctx, JSValue this_val) {
38
+ auto* shadow = unwrap_shadow_root(this_val);
39
+ if (!shadow) return JS_NULL;
40
+ return make_element(ctx, shadow->host);
41
+ }
42
+
43
+ JSValue js_shadow_get_mode(JSContext* ctx, JSValue this_val) {
44
+ auto* shadow = unwrap_shadow_root(this_val);
45
+ if (!shadow) return JS_NewString(ctx, "open");
46
+ return JS_NewString(ctx, shadow->mode == LXB_DOM_SHADOW_ROOT_MODE_CLOSED ? "closed" : "open");
47
+ }
48
+
49
+ JSValue js_shadow_get_innerHTML(JSContext* ctx, JSValue this_val) {
50
+ auto* shadow = unwrap_shadow_root(this_val);
51
+ if (!shadow) return JS_NewString(ctx, "");
52
+ std::string result;
53
+ lxb_dom_node_t* child = lxb_dom_interface_node(shadow)->first_child;
54
+ while (child) { result += serialize_node(child); child = child->next; }
55
+ return JS_NewString(ctx, result.c_str());
56
+ }
57
+
58
+ JSValue js_shadow_set_innerHTML(JSContext* ctx, JSValue this_val, JSValue val) {
59
+ auto* shadow = unwrap_shadow_root(this_val);
60
+ if (!shadow) return JS_UNDEFINED;
61
+ const char* html = JS_ToCString(ctx, val);
62
+ if (!html) return JS_UNDEFINED;
63
+
64
+ lxb_dom_node_t* node = lxb_dom_interface_node(shadow);
65
+ auto* rctx = get_ctx(ctx);
66
+ std::vector<void*> old_children;
67
+ lxb_dom_node_t* child = node->first_child;
68
+ while (child) { old_children.push_back(child); child = child->next; }
69
+ if (!old_children.empty()) invalidate_node_cache_deep_batch(ctx, rctx, old_children);
70
+
71
+ get_doc(ctx)->setInnerHTMLOnShadowRoot(shadow, shadow->host, html);
72
+ JS_FreeCString(ctx, html);
73
+ return JS_UNDEFINED;
74
+ }
75
+
76
+ JSValue js_shadow_querySelector(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
77
+ auto* shadow = unwrap_shadow_root(this_val);
78
+ if (!shadow || argc < 1) return JS_NULL;
79
+ const char* sel = JS_ToCString(ctx, argv[0]);
80
+ if (!sel) return JS_NULL;
81
+ void* found = get_doc(ctx)->querySelectorFromEl(shadow, sel);
82
+ JS_FreeCString(ctx, sel);
83
+ return make_element(ctx, found);
84
+ }
85
+
86
+ JSValue js_shadow_querySelectorAll(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
87
+ auto* shadow = unwrap_shadow_root(this_val);
88
+ if (!shadow || argc < 1) return JS_NewArray(ctx);
89
+ const char* sel = JS_ToCString(ctx, argv[0]);
90
+ if (!sel) return JS_NewArray(ctx);
91
+ auto found = get_doc(ctx)->querySelectorAllFromEl(shadow, sel);
92
+ JS_FreeCString(ctx, sel);
93
+ return make_element_array(ctx, found);
94
+ }
95
+
96
+ // ── Element.prototype.attachShadow() / .shadowRoot ────────────────────────────
97
+
98
+ JSValue js_el_attachShadow(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
99
+ auto* el = unwrap_element(ctx, this_val);
100
+ if (!el) return JS_UNDEFINED;
101
+
102
+ if (argc < 1 || !JS_IsObject(argv[0])) {
103
+ return JS_ThrowTypeError(ctx, "attachShadow() requires an options object with a 'mode' property");
104
+ }
105
+ JSValue mode_val = JS_GetPropertyStr(ctx, argv[0], "mode");
106
+ if (JS_IsException(mode_val)) return JS_EXCEPTION;
107
+ const char* mode_str = JS_ToCString(ctx, mode_val);
108
+ JS_FreeValue(ctx, mode_val);
109
+ if (!mode_str) return JS_EXCEPTION;
110
+ bool is_open = strcmp(mode_str, "open") == 0;
111
+ bool is_closed = strcmp(mode_str, "closed") == 0;
112
+ JS_FreeCString(ctx, mode_str);
113
+ if (!is_open && !is_closed) {
114
+ return JS_ThrowTypeError(ctx, "attachShadow() options.mode must be 'open' or 'closed'");
115
+ }
116
+
117
+ auto* rctx = get_ctx(ctx);
118
+ if (rctx && rctx->element_shadow_roots.count(el)) {
119
+ return throw_dom_exception(ctx, "NotSupportedError", "This element already has an attached shadow root.");
120
+ }
121
+
122
+ void* shadow = get_doc(ctx)->createShadowRoot(el, is_closed ? 1 : 0);
123
+ if (!shadow) return JS_NULL;
124
+ if (rctx) rctx->element_shadow_roots[el] = shadow;
125
+
126
+ return make_shadow_root(ctx, shadow);
127
+ }
128
+
129
+ JSValue js_el_get_shadowRoot(JSContext* ctx, JSValue this_val) {
130
+ auto* el = unwrap_element(ctx, this_val);
131
+ if (!el) return JS_NULL;
132
+ auto* rctx = get_ctx(ctx);
133
+ if (!rctx) return JS_NULL;
134
+ auto it = rctx->element_shadow_roots.find(el);
135
+ if (it == rctx->element_shadow_roots.end()) return JS_NULL;
136
+ auto* shadow = static_cast<lxb_dom_shadow_root_t*>(it->second);
137
+ if (shadow->mode == LXB_DOM_SHADOW_ROOT_MODE_CLOSED) return JS_NULL;
138
+ return make_shadow_root(ctx, shadow);
139
+ }
140
+
141
+ } // namespace
142
+
143
+ void ShadowRootBindings::install(JSContext* ctx) {
144
+ if (js_shadow_root_class_id == 0) JS_NewClassID(&js_shadow_root_class_id);
145
+ JS_NewClass(JS_GetRuntime(ctx), js_shadow_root_class_id, &js_shadow_root_class);
146
+
147
+ JSValue node_proto = JS_GetClassProto(ctx, js_node_class_id);
148
+ JSValue proto = JS_NewObjectProto(ctx, node_proto);
149
+ JS_FreeValue(ctx, node_proto);
150
+
151
+ define_prop(ctx, proto, "host", js_shadow_get_host, nullptr);
152
+ define_prop(ctx, proto, "mode", js_shadow_get_mode, nullptr);
153
+ define_prop(ctx, proto, "innerHTML", js_shadow_get_innerHTML, js_shadow_set_innerHTML);
154
+ JS_SetPropertyStr(ctx, proto, "querySelector",
155
+ JS_NewCFunction(ctx, js_shadow_querySelector, "querySelector", 1));
156
+ JS_SetPropertyStr(ctx, proto, "querySelectorAll",
157
+ JS_NewCFunction(ctx, js_shadow_querySelectorAll, "querySelectorAll", 1));
158
+
159
+ JS_SetClassProto(ctx, js_shadow_root_class_id, proto);
160
+ JSValue shadow_proto_ref = JS_GetClassProto(ctx, js_shadow_root_class_id);
161
+ JS_FreeValue(ctx, define_global_constructor(ctx, "ShadowRoot", shadow_proto_ref));
162
+ JS_FreeValue(ctx, shadow_proto_ref);
163
+
164
+ JSValue element_proto = JS_GetClassProto(ctx, js_element_class_id);
165
+ JS_SetPropertyStr(ctx, element_proto, "attachShadow",
166
+ JS_NewCFunction(ctx, js_el_attachShadow, "attachShadow", 1));
167
+ define_prop(ctx, element_proto, "shadowRoot", js_el_get_shadowRoot, nullptr);
168
+ JS_FreeValue(ctx, element_proto);
169
+ }
170
+
171
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers the ShadowRoot class (host/mode/innerHTML/querySelector(All),
8
+ // plus Node-proto methods like appendChild/childNodes via prototype chain)
9
+ // and Element.prototype.attachShadow()/shadowRoot.
10
+ //
11
+ // A ShadowRoot wraps a real lxb_dom_shadow_root_t*, which embeds a
12
+ // lxb_dom_document_fragment_t as its first member — so it shares the
13
+ // document fragment's node-tree layout, and generic Node methods work on it
14
+ // unmodified. Lexbor has no attachShadow-equivalent linking a host element
15
+ // to its shadow root, so that association is tracked in
16
+ // RuntimeContext::element_shadow_roots.
17
+ //
18
+ // No slotting/encapsulation/event retargeting — this models just enough of
19
+ // Shadow DOM for scripts that create/query/populate a scoped subtree.
20
+ //
21
+ // Must run after ElementBindings (globalThis.Element, js_node_class_id's
22
+ // proto) and DOMExceptionBindings (NotSupportedError on double-attach).
23
+ struct ShadowRootBindings {
24
+ static void install(JSContext* ctx);
25
+ };
26
+
27
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,161 @@
1
+ #include "SlotBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kSlotBootstrapScript = R"JS(
9
+ (function() {
10
+ function isSlotElement(el) {
11
+ return !!el && el.tagName === 'SLOT';
12
+ }
13
+
14
+ // Elements can carry a slot="..." attribute; anything else (text nodes,
15
+ // elements without the attribute) is assigned to the default slot ("").
16
+ function effectiveSlotName(node) {
17
+ if (node && node.nodeType === 1 && typeof node.getAttribute === 'function') {
18
+ return node.getAttribute('slot') || '';
19
+ }
20
+ return '';
21
+ }
22
+
23
+ // Walks up from a node living INSIDE a shadow tree (e.g. a <slot>) to find
24
+ // its containing ShadowRoot.
25
+ function containingShadowRoot(node) {
26
+ var n = node ? node.parentNode : null;
27
+ while (n) {
28
+ if (n instanceof ShadowRoot) return n;
29
+ n = n.parentNode;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // For a light-DOM child of some shadow host, returns that host's shadow
35
+ // root (or null if its parent has none / it's closed-mode).
36
+ function hostShadowRootFor(node) {
37
+ var parent = node ? node.parentNode : null;
38
+ return (parent && parent.shadowRoot) ? parent.shadowRoot : null;
39
+ }
40
+
41
+ function slotName(slotEl) {
42
+ return slotEl.getAttribute('name') || '';
43
+ }
44
+
45
+ // Nodes assigned to `slotEl`: light-DOM children of the shadow root's host
46
+ // whose effective slot name matches this slot's name, in host child order.
47
+ function computeAssigned(slotEl) {
48
+ var root = containingShadowRoot(slotEl);
49
+ if (!root) return [];
50
+ var host = root.host;
51
+ if (!host) return [];
52
+ var name = slotName(slotEl);
53
+ var result = [];
54
+ var child = host.firstChild;
55
+ while (child) {
56
+ if (effectiveSlotName(child) === name) result.push(child);
57
+ child = child.nextSibling;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ Element.prototype.assignedNodes = function(options) {
63
+ if (!isSlotElement(this)) return [];
64
+ var assigned = computeAssigned(this);
65
+ if (assigned.length === 0 && options && options.flatten) {
66
+ var fallback = [];
67
+ var c = this.firstChild;
68
+ while (c) { fallback.push(c); c = c.nextSibling; }
69
+ return fallback;
70
+ }
71
+ return assigned;
72
+ };
73
+
74
+ Element.prototype.assignedElements = function(options) {
75
+ return this.assignedNodes(options).filter(function(n) { return n.nodeType === 1; });
76
+ };
77
+
78
+ // ── slotchange (best-effort — see SlotBindings.hpp for exact triggers) ────
79
+
80
+ function sameAssignment(a, b) {
81
+ if (a.length !== b.length) return false;
82
+ for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
83
+ return true;
84
+ }
85
+
86
+ function notifySlotChange(root) {
87
+ if (!root || typeof root.querySelectorAll !== 'function') return;
88
+ var slots = root.querySelectorAll('slot');
89
+ for (var i = 0; i < slots.length; i++) {
90
+ var slot = slots[i];
91
+ var next = computeAssigned(slot);
92
+ var prev = slot.__slotAssigned || [];
93
+ if (!sameAssignment(prev, next)) {
94
+ slot.__slotAssigned = next;
95
+ slot.dispatchEvent(new Event('slotchange', { bubbles: true }));
96
+ }
97
+ }
98
+ }
99
+
100
+ var origAppendChild = Node.prototype.appendChild;
101
+ Node.prototype.appendChild = function(child) {
102
+ var result = origAppendChild.call(this, child);
103
+ if (this.shadowRoot) notifySlotChange(this.shadowRoot);
104
+ return result;
105
+ };
106
+
107
+ var origRemoveChild = Node.prototype.removeChild;
108
+ Node.prototype.removeChild = function(child) {
109
+ var result = origRemoveChild.call(this, child);
110
+ if (this.shadowRoot) notifySlotChange(this.shadowRoot);
111
+ return result;
112
+ };
113
+
114
+ var origSetAttribute = Element.prototype.setAttribute;
115
+ Element.prototype.setAttribute = function(name, value) {
116
+ var result = origSetAttribute.call(this, name, value);
117
+ if (String(name).toLowerCase() === 'slot') {
118
+ var root = hostShadowRootFor(this);
119
+ if (root) notifySlotChange(root);
120
+ }
121
+ return result;
122
+ };
123
+
124
+ var origRemoveAttribute = Element.prototype.removeAttribute;
125
+ Element.prototype.removeAttribute = function(name) {
126
+ var result = origRemoveAttribute.call(this, name);
127
+ if (String(name).toLowerCase() === 'slot') {
128
+ var root = hostShadowRootFor(this);
129
+ if (root) notifySlotChange(root);
130
+ }
131
+ return result;
132
+ };
133
+
134
+ var shadowInnerHTMLDesc = Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'innerHTML');
135
+ if (shadowInnerHTMLDesc && shadowInnerHTMLDesc.set) {
136
+ var origShadowInnerHTMLSet = shadowInnerHTMLDesc.set;
137
+ Object.defineProperty(ShadowRoot.prototype, 'innerHTML', {
138
+ get: shadowInnerHTMLDesc.get,
139
+ set: function(html) {
140
+ origShadowInnerHTMLSet.call(this, html);
141
+ notifySlotChange(this);
142
+ },
143
+ enumerable: shadowInnerHTMLDesc.enumerable,
144
+ configurable: true,
145
+ });
146
+ }
147
+ })();
148
+ )JS";
149
+
150
+ } // namespace
151
+
152
+ void SlotBindings::install(JSContext* ctx) {
153
+ JSValue result = JS_Eval(ctx, kSlotBootstrapScript, strlen(kSlotBootstrapScript),
154
+ "<slot-bootstrap>", JS_EVAL_TYPE_GLOBAL);
155
+ if (JS_IsException(result)) {
156
+ JS_FreeValue(ctx, JS_GetException(ctx));
157
+ }
158
+ JS_FreeValue(ctx, result);
159
+ }
160
+
161
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,44 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers <slot> assignment: Element.prototype.assignedNodes/
8
+ // assignedElements (tag-checked for "slot"), and a best-effort `slotchange`
9
+ // event. Pure JS, built entirely on top of already-exposed primitives
10
+ // (querySelectorAll, childNodes/parentNode, Element.prototype.shadowRoot,
11
+ // setAttribute/removeAttribute, ShadowRoot.prototype.innerHTML) via
12
+ // monkey-patching — Lexbor itself has no slot concept at all.
13
+ //
14
+ // Assignment algorithm: a light-DOM child of a shadow host is assigned to
15
+ // the <slot> (inside that host's shadow tree) whose `name` matches the
16
+ // child's `slot` attribute (or the unnamed/default slot, if the child has
17
+ // none), computed on demand from the live tree — never cached/stale.
18
+ //
19
+ // Scope/limitations (documented rather than silently wrong):
20
+ // - Does not implement the "first slot with a given name wins" tie-break for
21
+ // shadow trees with duplicate slot names — assumes one slot per name.
22
+ // - assignedNodes({flatten}) falls back to the slot's own light-DOM children
23
+ // (its fallback content) when it has no real assignment; it does not
24
+ // recursively flatten nested slots-inside-slots.
25
+ // - slotchange fires (best-effort, comparing each slot's previous vs.
26
+ // recomputed assignment) on: host.appendChild()/removeChild(), a
27
+ // light-DOM child's `slot` attribute changing via setAttribute/
28
+ // removeAttribute, and ShadowRoot.innerHTML assignment (covers newly
29
+ // added <slot> elements getting their first, non-empty assignment — the
30
+ // common attachShadow-then-populate pattern). NOT covered: before/after/
31
+ // replaceWith/append/prepend on the host, or mutating the shadow tree
32
+ // itself via appendChild/removeChild on the ShadowRoot directly.
33
+ // - Only reaches shadow roots visible via the public (mode-agnostic-for-us)
34
+ // Element.prototype.shadowRoot getter, so a *closed*-mode shadow host
35
+ // won't trigger slotchange from its light-DOM child mutations (the getter
36
+ // returns null for closed mode by spec) — assignedNodes()/assignedElements()
37
+ // on the <slot> itself are unaffected and still compute correctly.
38
+ //
39
+ // Must run after ElementBindings, EventBindings, and ShadowRootBindings.
40
+ struct SlotBindings {
41
+ static void install(JSContext* ctx);
42
+ };
43
+
44
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,42 @@
1
+ #include "TemplateBindings.hpp"
2
+ #include "../DOMBindingsInternal.hpp"
3
+ #include "../../lexbor/LexborDocument.hpp"
4
+ #include <lexbor/dom/dom.h>
5
+ #include <cstring>
6
+ #include <cctype>
7
+
8
+ namespace margelo::nitro::nitrojsdom {
9
+
10
+ namespace {
11
+
12
+ bool is_template_element(lxb_dom_element_t* el) {
13
+ size_t len = 0;
14
+ const lxb_char_t* name = lxb_dom_element_local_name(el, &len);
15
+ if (!name || len != 8) return false;
16
+ for (size_t i = 0; i < len; i++) {
17
+ if (std::tolower(name[i]) != "template"[i]) return false;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ JSValue js_el_get_content(JSContext* ctx, JSValue this_val) {
23
+ auto* el = unwrap_element(ctx, this_val);
24
+ if (!el || !is_template_element(el)) return JS_UNDEFINED;
25
+ return make_element(ctx, get_doc(ctx)->templateContent(el));
26
+ }
27
+
28
+ } // namespace
29
+
30
+ void TemplateBindings::install(JSContext* ctx) {
31
+ JSValue global = JS_GetGlobalObject(ctx);
32
+ JSValue element_ctor = JS_GetPropertyStr(ctx, global, "Element");
33
+ JSValue element_proto = JS_GetPropertyStr(ctx, element_ctor, "prototype");
34
+
35
+ define_prop(ctx, element_proto, "content", js_el_get_content, nullptr);
36
+
37
+ JS_FreeValue(ctx, element_proto);
38
+ JS_FreeValue(ctx, element_ctor);
39
+ JS_FreeValue(ctx, global);
40
+ }
41
+
42
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,29 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers a `content` getter on Element.prototype, tag-checked for
8
+ // "template" internally (mirroring FormBindings' tag-check pattern) —
9
+ // returns the lxb_dom_document_fragment_t Lexbor already allocates as
10
+ // lxb_html_template_element_t::content, for both parsed and
11
+ // document.createElement()'d <template> elements. No custom parsing logic:
12
+ // Lexbor's own HTML5 tree constructor already redirects a parsed
13
+ // <template>'s children into that fragment instead of the template's own
14
+ // light-DOM children (see packages/lexbor/.../tree/insertion_mode/in_template.c).
15
+ // LexborDocument::setInnerHTMLOnEl() mirrors that for template.innerHTML=.
16
+ //
17
+ // Scope: the returned fragment is a generic Node wrapper (like
18
+ // document.createDocumentFragment()), so it has childNodes/appendChild/etc.
19
+ // but not querySelector(All) — same limitation createDocumentFragment()
20
+ // already has. cloneNode() on a <template> does not specially deep-clone
21
+ // `.content` (relies on Lexbor's own node clone, not verified against spec
22
+ // for this case) — undocumented/untested edge case, not the golden path.
23
+ //
24
+ // Must run after ElementBindings (globalThis.Element, unwrap_element).
25
+ struct TemplateBindings {
26
+ static void install(JSContext* ctx);
27
+ };
28
+
29
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,117 @@
1
+ #include "TextEncodingBindings.hpp"
2
+ #include <cstring>
3
+
4
+ namespace margelo::nitro::nitrojsdom {
5
+
6
+ namespace {
7
+
8
+ const char* kTextEncodingBootstrapScript = R"JS(
9
+ (function() {
10
+ function TextEncoder() {
11
+ this.encoding = 'utf-8';
12
+ }
13
+ TextEncoder.prototype.encode = function(input) {
14
+ var str = String(input === undefined ? '' : input);
15
+ var bytes = [];
16
+ for (var i = 0; i < str.length; i++) {
17
+ var code = str.charCodeAt(i);
18
+ if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
19
+ var next = str.charCodeAt(i + 1);
20
+ if (next >= 0xDC00 && next <= 0xDFFF) {
21
+ code = ((code - 0xD800) << 10) + (next - 0xDC00) + 0x10000;
22
+ i++;
23
+ }
24
+ }
25
+ if (code < 0x80) {
26
+ bytes.push(code);
27
+ } else if (code < 0x800) {
28
+ bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
29
+ } else if (code < 0x10000) {
30
+ bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
31
+ } else {
32
+ bytes.push(
33
+ 0xF0 | (code >> 18),
34
+ 0x80 | ((code >> 12) & 0x3F),
35
+ 0x80 | ((code >> 6) & 0x3F),
36
+ 0x80 | (code & 0x3F)
37
+ );
38
+ }
39
+ }
40
+ return new Uint8Array(bytes);
41
+ };
42
+ TextEncoder.prototype.encodeInto = function(input, dest) {
43
+ var encoded = this.encode(input);
44
+ var written = Math.min(encoded.length, dest.length);
45
+ for (var i = 0; i < written; i++) dest[i] = encoded[i];
46
+ return { read: written === encoded.length ? String(input).length : written, written: written };
47
+ };
48
+
49
+ function TextDecoder(label, options) {
50
+ this.encoding = label ? String(label).toLowerCase() : 'utf-8';
51
+ this.fatal = !!(options && options.fatal);
52
+ this.ignoreBOM = !!(options && options.ignoreBOM);
53
+ }
54
+ TextDecoder.prototype.decode = function(input) {
55
+ var bytes;
56
+ if (input === undefined) {
57
+ bytes = new Uint8Array(0);
58
+ } else if (input instanceof ArrayBuffer) {
59
+ bytes = new Uint8Array(input);
60
+ } else if (input && input.buffer !== undefined) {
61
+ bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
62
+ } else {
63
+ bytes = new Uint8Array(0);
64
+ }
65
+
66
+ var result = '';
67
+ var i = 0;
68
+ while (i < bytes.length) {
69
+ var b0 = bytes[i];
70
+ var codepoint, len;
71
+ if (b0 < 0x80) { codepoint = b0; len = 1; }
72
+ else if ((b0 & 0xE0) === 0xC0) { codepoint = b0 & 0x1F; len = 2; }
73
+ else if ((b0 & 0xF0) === 0xE0) { codepoint = b0 & 0x0F; len = 3; }
74
+ else if ((b0 & 0xF8) === 0xF0) { codepoint = b0 & 0x07; len = 4; }
75
+ else {
76
+ if (this.fatal) throw new TypeError('The encoded data was not valid UTF-8.');
77
+ result += '�';
78
+ i++;
79
+ continue;
80
+ }
81
+ if (i + len > bytes.length) {
82
+ if (this.fatal) throw new TypeError('The encoded data was not valid UTF-8.');
83
+ result += '�';
84
+ break;
85
+ }
86
+ for (var k = 1; k < len; k++) {
87
+ codepoint = (codepoint << 6) | (bytes[i + k] & 0x3F);
88
+ }
89
+ if (codepoint > 0xFFFF) {
90
+ codepoint -= 0x10000;
91
+ result += String.fromCharCode(0xD800 + (codepoint >> 10), 0xDC00 + (codepoint & 0x3FF));
92
+ } else {
93
+ result += String.fromCharCode(codepoint);
94
+ }
95
+ i += len;
96
+ }
97
+ if (!this.ignoreBOM && result.charCodeAt(0) === 0xFEFF) result = result.slice(1);
98
+ return result;
99
+ };
100
+
101
+ globalThis.TextEncoder = TextEncoder;
102
+ globalThis.TextDecoder = TextDecoder;
103
+ })();
104
+ )JS";
105
+
106
+ } // namespace
107
+
108
+ void TextEncodingBindings::install(JSContext* ctx) {
109
+ JSValue result = JS_Eval(ctx, kTextEncodingBootstrapScript, strlen(kTextEncodingBootstrapScript),
110
+ "<text-encoding-bootstrap>", JS_EVAL_TYPE_GLOBAL);
111
+ if (JS_IsException(result)) {
112
+ JS_FreeValue(ctx, JS_GetException(ctx));
113
+ }
114
+ JS_FreeValue(ctx, result);
115
+ }
116
+
117
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #include "quickjs.h"
4
+
5
+ namespace margelo::nitro::nitrojsdom {
6
+
7
+ // Registers globalThis.TextEncoder/TextDecoder (UTF-8 only — the vast
8
+ // majority of real-world usage — implemented in pure JS since encoding a
9
+ // JS string's UTF-16 code units to UTF-8 bytes needs no native primitive).
10
+ struct TextEncodingBindings {
11
+ static void install(JSContext* ctx);
12
+ };
13
+
14
+ } // namespace margelo::nitro::nitrojsdom
@@ -2,6 +2,7 @@
2
2
  #include "../DOMBindingsInternal.hpp"
3
3
  #include "../QuickJSRuntime.hpp"
4
4
  #include <chrono>
5
+ #include <cstring>
5
6
 
6
7
  namespace margelo::nitro::nitrojsdom {
7
8
 
@@ -12,6 +13,17 @@ int64_t dom_now_ms() {
12
13
  return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
13
14
  }
14
15
 
16
+ double dom_now_high_res_ms() {
17
+ using namespace std::chrono;
18
+ return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
19
+ }
20
+
21
+ JSValue js_native_now_ms(JSContext* ctx, JSValue, int, JSValue*) {
22
+ auto* rctx = get_ctx(ctx);
23
+ double origin = rctx ? rctx->time_origin_ms : 0.0;
24
+ return JS_NewFloat64(ctx, dom_now_high_res_ms() - origin);
25
+ }
26
+
15
27
  JSValue js_setTimeout(JSContext* ctx, JSValue, int argc, JSValue* argv) {
16
28
  if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_NewInt32(ctx, 0);
17
29
  auto* rctx = get_ctx(ctx);
@@ -80,15 +92,44 @@ JSValue js_clearTimer(JSContext* ctx, JSValue, int argc, JSValue* argv) {
80
92
  return JS_UNDEFINED;
81
93
  }
82
94
 
95
+ const char* kTimerBootstrapScript = R"JS(
96
+ (function() {
97
+ globalThis.performance = globalThis.performance || {};
98
+ globalThis.performance.now = function() { return __nativeNowMs(); };
99
+
100
+ globalThis.requestAnimationFrame = function(callback) {
101
+ return setTimeout(function() { callback(performance.now()); }, 16);
102
+ };
103
+ globalThis.cancelAnimationFrame = function(id) {
104
+ clearTimeout(id);
105
+ };
106
+
107
+ globalThis.queueMicrotask = function(callback) {
108
+ Promise.resolve().then(function() { callback(); });
109
+ };
110
+ })();
111
+ )JS";
112
+
83
113
  } // namespace
84
114
 
85
115
  void TimerBindings::install(JSContext* ctx) {
116
+ auto* rctx = get_ctx(ctx);
117
+ if (rctx) rctx->time_origin_ms = dom_now_high_res_ms();
118
+
86
119
  JSValue global = JS_GetGlobalObject(ctx);
87
120
  JS_SetPropertyStr(ctx, global, "setTimeout", JS_NewCFunction(ctx, js_setTimeout, "setTimeout", 2));
88
121
  JS_SetPropertyStr(ctx, global, "setInterval", JS_NewCFunction(ctx, js_setInterval, "setInterval", 2));
89
122
  JS_SetPropertyStr(ctx, global, "clearTimeout", JS_NewCFunction(ctx, js_clearTimer, "clearTimeout", 1));
90
123
  JS_SetPropertyStr(ctx, global, "clearInterval", JS_NewCFunction(ctx, js_clearTimer, "clearInterval",1));
124
+ JS_SetPropertyStr(ctx, global, "__nativeNowMs", JS_NewCFunction(ctx, js_native_now_ms, "__nativeNowMs", 0));
91
125
  JS_FreeValue(ctx, global);
126
+
127
+ JSValue result = JS_Eval(ctx, kTimerBootstrapScript, strlen(kTimerBootstrapScript),
128
+ "<timer-bootstrap>", JS_EVAL_TYPE_GLOBAL);
129
+ if (JS_IsException(result)) {
130
+ JS_FreeValue(ctx, JS_GetException(ctx));
131
+ }
132
+ JS_FreeValue(ctx, result);
92
133
  }
93
134
 
94
135
  } // namespace margelo::nitro::nitrojsdom