@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.
- package/android/CMakeLists.txt +14 -0
- package/cpp/HybridHtmlSandbox.cpp +16 -3
- package/cpp/HybridHtmlSandbox.hpp +1 -1
- package/cpp/lexbor/LexborDocument.cpp +217 -67
- package/cpp/lexbor/LexborDocument.hpp +29 -10
- package/cpp/quickjs/DOMBindings.cpp +28 -0
- package/cpp/quickjs/DOMBindingsInternal.cpp +34 -2
- package/cpp/quickjs/DOMBindingsInternal.hpp +17 -1
- package/cpp/quickjs/QuickJSRuntime.cpp +19 -4
- package/cpp/quickjs/QuickJSRuntime.hpp +22 -1
- package/cpp/quickjs/bindings/AbortBindings.cpp +114 -0
- package/cpp/quickjs/bindings/AbortBindings.hpp +12 -0
- package/cpp/quickjs/bindings/BlobBindings.cpp +137 -0
- package/cpp/quickjs/bindings/BlobBindings.hpp +14 -0
- package/cpp/quickjs/bindings/CSSOMBindings.cpp +244 -0
- package/cpp/quickjs/bindings/CSSOMBindings.hpp +23 -0
- package/cpp/quickjs/bindings/ClassListBindings.cpp +3 -2
- package/cpp/quickjs/bindings/CookieBindings.cpp +118 -0
- package/cpp/quickjs/bindings/CookieBindings.hpp +23 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.cpp +341 -0
- package/cpp/quickjs/bindings/CustomElementsBindings.hpp +50 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.cpp +111 -0
- package/cpp/quickjs/bindings/DOMExceptionBindings.hpp +20 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +84 -4
- package/cpp/quickjs/bindings/ElementBindings.cpp +264 -28
- package/cpp/quickjs/bindings/EventBindings.cpp +137 -3
- package/cpp/quickjs/bindings/FetchBindings.cpp +5 -1
- package/cpp/quickjs/bindings/FormBindings.cpp +142 -0
- package/cpp/quickjs/bindings/FormBindings.hpp +16 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +183 -0
- package/cpp/quickjs/bindings/LiveCollectionBindings.hpp +16 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.cpp +171 -0
- package/cpp/quickjs/bindings/ShadowRootBindings.hpp +27 -0
- package/cpp/quickjs/bindings/SlotBindings.cpp +161 -0
- package/cpp/quickjs/bindings/SlotBindings.hpp +44 -0
- package/cpp/quickjs/bindings/TemplateBindings.cpp +42 -0
- package/cpp/quickjs/bindings/TemplateBindings.hpp +29 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.cpp +117 -0
- package/cpp/quickjs/bindings/TextEncodingBindings.hpp +14 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +41 -0
- package/cpp/quickjs/bindings/UrlBindings.cpp +286 -0
- package/cpp/quickjs/bindings/UrlBindings.hpp +12 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +270 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.cpp +44 -0
- package/cpp/quickjs/bindings/XmlSerializerBindings.hpp +24 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js +1 -1
- package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -1
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +1 -1
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +1 -1
- package/package.json +1 -1
- package/src/classes/JSDOM/JSDOM.class.ts +1 -0
- 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
|