@async/framework 0.3.0 → 0.5.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/CHANGELOG.md +28 -0
- package/README.md +254 -34
- package/examples/components/main.js +10 -10
- package/examples/ssr/main.js +1 -1
- package/examples/streaming/main.js +1 -1
- package/framework.js +4133 -0
- package/package.json +9 -5
- package/src/async-signal.js +12 -1
- package/src/attributes.js +2 -0
- package/src/cache.js +5 -0
- package/src/component.js +149 -17
- package/src/handlers.js +12 -4
- package/src/html.js +99 -6
- package/src/loader.js +311 -18
- package/src/partials.js +20 -7
- package/src/registry-store.js +4 -0
- package/src/router.js +9 -0
- package/src/server.js +26 -6
- package/src/signals.js +16 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "No-build AsyncLoader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.1.0",
|
|
@@ -28,19 +28,21 @@
|
|
|
28
28
|
"web-framework"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
-
"unpkg": "./
|
|
31
|
+
"unpkg": "./framework.js",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
-
"unpkg": "./
|
|
35
|
-
"browser": "./
|
|
34
|
+
"unpkg": "./framework.js",
|
|
35
|
+
"browser": "./framework.js",
|
|
36
36
|
"import": "./src/index.js",
|
|
37
37
|
"default": "./src/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./framework.js": "./framework.js",
|
|
39
40
|
"./package.json": "./package.json"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"CHANGELOG.md",
|
|
43
44
|
"README.md",
|
|
45
|
+
"framework.js",
|
|
44
46
|
"src",
|
|
45
47
|
"examples",
|
|
46
48
|
"LICENSE",
|
|
@@ -48,10 +50,12 @@
|
|
|
48
50
|
],
|
|
49
51
|
"scripts": {
|
|
50
52
|
"async-pipeline": "async-pipeline",
|
|
53
|
+
"bundle": "node scripts/build-framework-bundle.js",
|
|
54
|
+
"bundle:check": "node scripts/build-framework-bundle.js --check",
|
|
51
55
|
"docs:build": "node scripts/build-pages.js",
|
|
52
56
|
"examples": "node --test tests/examples.test.js",
|
|
53
57
|
"examples:check": "node --test tests/examples.test.js",
|
|
54
|
-
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
58
|
+
"pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
|
|
55
59
|
"pipeline:github:check": "async-pipeline github check",
|
|
56
60
|
"pipeline:github:generate": "async-pipeline github generate",
|
|
57
61
|
"pipeline:pages": "async-pipeline run pages",
|
package/src/async-signal.js
CHANGED
|
@@ -82,7 +82,18 @@ export function asyncSignal(id, fn) {
|
|
|
82
82
|
signals: registry,
|
|
83
83
|
id: registeredId,
|
|
84
84
|
get server() {
|
|
85
|
-
|
|
85
|
+
const context = registry._context?.() ?? {};
|
|
86
|
+
const server = context.server;
|
|
87
|
+
if (typeof server?._withContext === "function") {
|
|
88
|
+
return server._withContext({
|
|
89
|
+
signals: registry,
|
|
90
|
+
router: context.router,
|
|
91
|
+
loader: context.loader,
|
|
92
|
+
cache: context.cache,
|
|
93
|
+
abort: activeAbort
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return server;
|
|
86
97
|
},
|
|
87
98
|
get router() {
|
|
88
99
|
return registry._context?.().router;
|
package/src/attributes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const defaultPrefixes = Object.freeze({
|
|
2
2
|
async: ["async:"],
|
|
3
|
+
class: ["class:"],
|
|
3
4
|
signal: ["signal:"],
|
|
4
5
|
on: ["on:"]
|
|
5
6
|
});
|
|
@@ -11,6 +12,7 @@ export function defineAttributeConfig(config = {}) {
|
|
|
11
12
|
export function normalizeAttributeConfig(config = {}) {
|
|
12
13
|
return {
|
|
13
14
|
async: normalizePrefixes(config.async, defaultPrefixes.async),
|
|
15
|
+
class: normalizePrefixes(config.class, defaultPrefixes.class),
|
|
14
16
|
signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
|
|
15
17
|
on: normalizePrefixes(config.on, defaultPrefixes.on)
|
|
16
18
|
};
|
package/src/cache.js
CHANGED
|
@@ -34,6 +34,11 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
|
|
|
34
34
|
return registryApi;
|
|
35
35
|
},
|
|
36
36
|
|
|
37
|
+
unregister(id) {
|
|
38
|
+
assertId(id);
|
|
39
|
+
return definitions.delete(id);
|
|
40
|
+
},
|
|
41
|
+
|
|
37
42
|
resolve(id) {
|
|
38
43
|
assertId(id);
|
|
39
44
|
return definitions.get(id);
|
package/src/component.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attributeName } from "./attributes.js";
|
|
2
|
+
import { escapeHtml, rawHtml, renderTemplate } from "./html.js";
|
|
2
3
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
3
4
|
|
|
4
5
|
const componentKind = Symbol.for("@async/framework.component");
|
|
@@ -44,6 +45,13 @@ export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
|
44
45
|
return registry;
|
|
45
46
|
},
|
|
46
47
|
|
|
48
|
+
unregister(id) {
|
|
49
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
50
|
+
throw new TypeError("Component id must be a non-empty string.");
|
|
51
|
+
}
|
|
52
|
+
return entries.delete(id);
|
|
53
|
+
},
|
|
54
|
+
|
|
47
55
|
resolve(id) {
|
|
48
56
|
if (typeof id !== "string" || id.length === 0) {
|
|
49
57
|
throw new TypeError("Component id must be a non-empty string.");
|
|
@@ -71,29 +79,49 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
71
79
|
|
|
72
80
|
const scope = `${parentScope}.${componentName(Component)}.${++componentCounter}`;
|
|
73
81
|
const cleanups = [];
|
|
74
|
-
const
|
|
82
|
+
const attachHooks = [];
|
|
75
83
|
const visibleHooks = [];
|
|
84
|
+
const destroyHooks = [];
|
|
85
|
+
const bindingIds = [];
|
|
86
|
+
const templateOptions = {
|
|
87
|
+
attributes: runtime.attributes,
|
|
88
|
+
signals: runtime.signals,
|
|
89
|
+
bind(value) {
|
|
90
|
+
const id = runtime.loader?._registerBinding?.(value);
|
|
91
|
+
if (!id) {
|
|
92
|
+
throw new Error("Inline template bindings require an AsyncLoader.");
|
|
93
|
+
}
|
|
94
|
+
bindingIds.push(id);
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const renderScopedTemplate = (value) => renderTemplate(value, templateOptions);
|
|
76
99
|
const context = createComponentContext({
|
|
77
100
|
runtime,
|
|
78
101
|
scope,
|
|
79
102
|
cleanups,
|
|
80
|
-
|
|
81
|
-
visibleHooks
|
|
103
|
+
attachHooks,
|
|
104
|
+
visibleHooks,
|
|
105
|
+
destroyHooks,
|
|
106
|
+
renderScopedTemplate
|
|
82
107
|
});
|
|
83
108
|
|
|
84
109
|
const output = Component.call(context, props);
|
|
85
|
-
const html =
|
|
110
|
+
const html = renderScopedTemplate(output);
|
|
86
111
|
|
|
87
112
|
return {
|
|
88
113
|
html,
|
|
89
|
-
|
|
90
|
-
for (const hook of
|
|
114
|
+
attach(target) {
|
|
115
|
+
for (const hook of attachHooks) {
|
|
91
116
|
const cleanup = hook(target);
|
|
92
117
|
if (typeof cleanup === "function") {
|
|
93
118
|
cleanups.push(cleanup);
|
|
94
119
|
}
|
|
95
120
|
}
|
|
96
121
|
},
|
|
122
|
+
mount(target) {
|
|
123
|
+
this.attach(target);
|
|
124
|
+
},
|
|
97
125
|
visible(target, observeVisible) {
|
|
98
126
|
for (const hook of visibleHooks) {
|
|
99
127
|
const cleanup = observeVisible(target, hook);
|
|
@@ -103,15 +131,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
103
131
|
}
|
|
104
132
|
},
|
|
105
133
|
cleanup() {
|
|
134
|
+
while (destroyHooks.length > 0) {
|
|
135
|
+
destroyHooks.pop()?.();
|
|
136
|
+
}
|
|
106
137
|
while (cleanups.length > 0) {
|
|
107
138
|
cleanups.pop()?.();
|
|
108
139
|
}
|
|
140
|
+
while (bindingIds.length > 0) {
|
|
141
|
+
runtime.loader?._releaseBinding?.(bindingIds.pop());
|
|
142
|
+
}
|
|
109
143
|
}
|
|
110
144
|
};
|
|
111
145
|
}
|
|
112
146
|
|
|
113
|
-
function createComponentContext({ runtime, scope, cleanups,
|
|
147
|
+
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
|
|
114
148
|
const { signals, handlers, loader, server, router, cache } = runtime;
|
|
149
|
+
const generatedHandlers = new WeakMap();
|
|
150
|
+
let generatedHandlerCounter = 0;
|
|
151
|
+
let generatedSignalCounter = 0;
|
|
115
152
|
const context = {
|
|
116
153
|
scope,
|
|
117
154
|
signals,
|
|
@@ -122,12 +159,28 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
122
159
|
cache,
|
|
123
160
|
|
|
124
161
|
signal(name, initial) {
|
|
125
|
-
|
|
162
|
+
if (arguments.length === 1) {
|
|
163
|
+
const id = scoped(scope, `signal.${++generatedSignalCounter}`);
|
|
164
|
+
const ref = signals.ensure(id, name);
|
|
165
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
166
|
+
return ref;
|
|
167
|
+
}
|
|
168
|
+
const id = scoped(scope, name);
|
|
169
|
+
const created = !signals.has(id);
|
|
170
|
+
const ref = signals.ensure(id, initial);
|
|
171
|
+
if (created) {
|
|
172
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
173
|
+
}
|
|
174
|
+
return ref;
|
|
126
175
|
},
|
|
127
176
|
|
|
128
177
|
computed(name, fn) {
|
|
129
178
|
const id = scoped(scope, name);
|
|
179
|
+
const created = !signals.has(id);
|
|
130
180
|
const ref = signals.ensure(id, undefined);
|
|
181
|
+
if (created) {
|
|
182
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
183
|
+
}
|
|
131
184
|
const cleanup = signals.effect(() => {
|
|
132
185
|
signals.set(id, fn.call(context));
|
|
133
186
|
});
|
|
@@ -137,9 +190,13 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
137
190
|
|
|
138
191
|
asyncSignal(name, fn) {
|
|
139
192
|
const id = scoped(scope, name);
|
|
193
|
+
const created = !signals.has(id);
|
|
140
194
|
if (!signals.has(id)) {
|
|
141
195
|
signals.asyncSignal(id, fn);
|
|
142
196
|
}
|
|
197
|
+
if (created) {
|
|
198
|
+
cleanups.push(() => signals.unregister?.(id));
|
|
199
|
+
}
|
|
143
200
|
return signals.ref(id);
|
|
144
201
|
},
|
|
145
202
|
|
|
@@ -150,31 +207,91 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
150
207
|
},
|
|
151
208
|
|
|
152
209
|
handler(name, fn) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
210
|
+
if (typeof name === "function" && fn === undefined) {
|
|
211
|
+
const inlineFn = name;
|
|
212
|
+
if (generatedHandlers.has(inlineFn)) {
|
|
213
|
+
return generatedHandlers.get(inlineFn);
|
|
214
|
+
}
|
|
215
|
+
const id = registerScopedHandler(`handler.${++generatedHandlerCounter}`, inlineFn);
|
|
216
|
+
generatedHandlers.set(inlineFn, id);
|
|
217
|
+
return id;
|
|
218
|
+
}
|
|
219
|
+
if (typeof fn !== "function") {
|
|
220
|
+
throw new TypeError("this.handler(name, fn) or this.handler(fn) requires a function.");
|
|
221
|
+
}
|
|
222
|
+
return registerScopedHandler(name, fn);
|
|
158
223
|
},
|
|
159
224
|
|
|
160
225
|
render(Child, childProps = {}) {
|
|
161
226
|
const child = renderComponent(Child, childProps, runtime, scope);
|
|
162
227
|
cleanups.push(child.cleanup);
|
|
163
|
-
|
|
228
|
+
attachHooks.push((target) => child.attach(target));
|
|
164
229
|
visibleHooks.push((target) => child.visible(target, loader._observeVisible));
|
|
165
230
|
return rawHtml(child.html);
|
|
166
231
|
},
|
|
167
232
|
|
|
233
|
+
suspense(signalRef, views) {
|
|
234
|
+
const id = signalRef?.id;
|
|
235
|
+
if (!id) {
|
|
236
|
+
throw new TypeError("this.suspense(signalRef, views) requires a signal ref.");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const normalized = normalizeSuspenseViews(views);
|
|
240
|
+
const chunks = [];
|
|
241
|
+
for (const state of ["loading", "ready", "error"]) {
|
|
242
|
+
const view = normalized[state];
|
|
243
|
+
if (!view) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const attr = attributeName(runtime.attributes, "async", state);
|
|
247
|
+
const body = renderScopedTemplate(view.call(context, signalRef));
|
|
248
|
+
chunks.push(`<template ${attr}="${escapeHtml(id)}">${body}</template>`);
|
|
249
|
+
}
|
|
250
|
+
return rawHtml(chunks.join(""));
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
on(eventName, fn) {
|
|
254
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
255
|
+
throw new TypeError("Component lifecycle event must be a non-empty string.");
|
|
256
|
+
}
|
|
257
|
+
if (typeof fn !== "function") {
|
|
258
|
+
throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
|
|
259
|
+
}
|
|
260
|
+
const event = eventName === "mount" ? "attach" : eventName;
|
|
261
|
+
if (event === "attach") {
|
|
262
|
+
attachHooks.push((target) => fn.call(context, target));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (event === "visible") {
|
|
266
|
+
visibleHooks.push((target) => fn.call(context, target));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (event === "destroy") {
|
|
270
|
+
destroyHooks.push(() => fn.call(context));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`Unsupported component lifecycle event "${eventName}".`);
|
|
274
|
+
},
|
|
275
|
+
|
|
168
276
|
onMount(fn) {
|
|
169
|
-
|
|
277
|
+
context.on("attach", fn);
|
|
170
278
|
},
|
|
171
279
|
|
|
172
280
|
onVisible(fn) {
|
|
173
|
-
|
|
281
|
+
context.on("visible", fn);
|
|
174
282
|
}
|
|
175
283
|
};
|
|
176
284
|
|
|
177
285
|
return context;
|
|
286
|
+
|
|
287
|
+
function registerScopedHandler(name, fn) {
|
|
288
|
+
const id = scoped(scope, name);
|
|
289
|
+
handlers.register(id, function runComponentHandler(handlerContext) {
|
|
290
|
+
return fn.call({ ...context, ...handlerContext }, handlerContext);
|
|
291
|
+
});
|
|
292
|
+
cleanups.push(() => handlers.unregister?.(id));
|
|
293
|
+
return id;
|
|
294
|
+
}
|
|
178
295
|
}
|
|
179
296
|
|
|
180
297
|
function scoped(scope, name) {
|
|
@@ -184,6 +301,21 @@ function scoped(scope, name) {
|
|
|
184
301
|
return `${scope}.${name}`;
|
|
185
302
|
}
|
|
186
303
|
|
|
304
|
+
function normalizeSuspenseViews(views) {
|
|
305
|
+
const normalized = typeof views === "function" ? { ready: views } : views;
|
|
306
|
+
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
|
|
307
|
+
throw new TypeError("this.suspense(signalRef, views) requires views to be a function or object.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const state of ["loading", "ready", "error"]) {
|
|
311
|
+
if (Object.hasOwn(normalized, state) && normalized[state] !== undefined && typeof normalized[state] !== "function") {
|
|
312
|
+
throw new TypeError(`this.suspense(signalRef, views) view "${state}" must be a function.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
318
|
+
|
|
187
319
|
function componentName(Component) {
|
|
188
320
|
return Component.displayName || Component.name || "anonymous";
|
|
189
321
|
}
|
package/src/handlers.js
CHANGED
|
@@ -6,11 +6,10 @@ import {
|
|
|
6
6
|
} from "./server.js";
|
|
7
7
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
8
8
|
|
|
9
|
-
const builtInTokens = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
9
|
+
const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
10
10
|
const builtInHandlers = {
|
|
11
|
-
preventDefault
|
|
12
|
-
|
|
13
|
-
},
|
|
11
|
+
prevent: preventDefault,
|
|
12
|
+
preventDefault,
|
|
14
13
|
stopPropagation() {
|
|
15
14
|
this.event?.stopPropagation?.();
|
|
16
15
|
},
|
|
@@ -19,6 +18,10 @@ const builtInHandlers = {
|
|
|
19
18
|
}
|
|
20
19
|
};
|
|
21
20
|
|
|
21
|
+
function preventDefault() {
|
|
22
|
+
this.event?.preventDefault?.();
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
23
26
|
const registryStore = options.registry ?? createRegistryStore();
|
|
24
27
|
const type = options.type ?? "handler";
|
|
@@ -44,6 +47,11 @@ export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
|
44
47
|
return registry;
|
|
45
48
|
},
|
|
46
49
|
|
|
50
|
+
unregister(id) {
|
|
51
|
+
assertId(id);
|
|
52
|
+
return handlers.delete(id);
|
|
53
|
+
},
|
|
54
|
+
|
|
47
55
|
resolve(id) {
|
|
48
56
|
assertId(id);
|
|
49
57
|
return handlers.get(id);
|
package/src/html.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSignalRef } from "./signals.js";
|
|
2
|
+
import { attributeName, matchAttribute, normalizeAttributeConfig } from "./attributes.js";
|
|
2
3
|
|
|
3
4
|
const templateKind = Symbol.for("@async/framework.template");
|
|
4
5
|
const rawKind = Symbol.for("@async/framework.rawHtml");
|
|
@@ -22,29 +23,36 @@ export function rawHtml(value) {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function renderTemplate(value) {
|
|
26
|
+
export function renderTemplate(value, options = {}) {
|
|
26
27
|
if (isTemplateResult(value)) {
|
|
28
|
+
const context = createRenderContext(options);
|
|
27
29
|
let output = "";
|
|
28
30
|
for (let index = 0; index < value.strings.length; index += 1) {
|
|
29
31
|
output += value.strings[index];
|
|
30
32
|
if (index < value.values.length) {
|
|
31
|
-
output += renderValue(value.values[index]
|
|
33
|
+
output += renderValue(value.values[index], {
|
|
34
|
+
...context,
|
|
35
|
+
attribute: readAttributeContext(value.strings[index])
|
|
36
|
+
});
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
return output;
|
|
35
40
|
}
|
|
36
|
-
return renderValue(value);
|
|
41
|
+
return renderValue(value, createRenderContext(options));
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function renderValue(value) {
|
|
44
|
+
function renderValue(value, context = createRenderContext()) {
|
|
40
45
|
if (value?.[rawKind]) {
|
|
41
46
|
return value.html;
|
|
42
47
|
}
|
|
43
48
|
if (isTemplateResult(value)) {
|
|
44
|
-
return renderTemplate(value);
|
|
49
|
+
return renderTemplate(value, context);
|
|
50
|
+
}
|
|
51
|
+
if (context.attribute) {
|
|
52
|
+
return renderAttributeValue(value, context);
|
|
45
53
|
}
|
|
46
54
|
if (Array.isArray(value)) {
|
|
47
|
-
return value.map(renderValue).join("");
|
|
55
|
+
return value.map((item) => renderValue(item, context)).join("");
|
|
48
56
|
}
|
|
49
57
|
if (isSignalRef(value)) {
|
|
50
58
|
return escapeHtml(value.value);
|
|
@@ -55,6 +63,91 @@ function renderValue(value) {
|
|
|
55
63
|
return escapeHtml(value);
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
function renderAttributeValue(value, context) {
|
|
67
|
+
const signalName = matchAttribute(context.attribute.name, context.attributes, "signal");
|
|
68
|
+
const className = matchAttribute(context.attribute.name, context.attributes, "class");
|
|
69
|
+
const signalPath = signalPathFor(value, context);
|
|
70
|
+
|
|
71
|
+
if (context.attribute.name === "value" && signalPath) {
|
|
72
|
+
const currentValue = readSignalValue(value, context);
|
|
73
|
+
const signalValueAttribute = attributeName(context.attributes, "signal", "value");
|
|
74
|
+
return `${escapeHtml(currentValue)}${context.attribute.quote} ${signalValueAttribute}=${context.attribute.quote}${escapeHtml(signalPath)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (signalName != null || className != null) {
|
|
78
|
+
if (signalPath) {
|
|
79
|
+
return escapeHtml(signalPath);
|
|
80
|
+
}
|
|
81
|
+
if (isInlineBindingValue(value)) {
|
|
82
|
+
return escapeHtml(registerInlineBinding(value, context));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return renderValueAsAttributeLiteral(value, context);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderValueAsAttributeLiteral(value, context) {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return value.map((item) => renderValueAsAttributeLiteral(item, context)).join("");
|
|
92
|
+
}
|
|
93
|
+
if (isSignalRef(value)) {
|
|
94
|
+
return escapeHtml(value.value);
|
|
95
|
+
}
|
|
96
|
+
if (value == null || value === false) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
return escapeHtml(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createRenderContext(options = {}) {
|
|
103
|
+
return {
|
|
104
|
+
...options,
|
|
105
|
+
attributes: normalizeAttributeConfig(options.attributes)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAttributeContext(source) {
|
|
110
|
+
const match = source.match(/(?:^|[\s<])([^\s"'=<>`]+)\s*=\s*(["'])$/);
|
|
111
|
+
if (!match) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
name: match[1],
|
|
116
|
+
quote: match[2]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function signalPathFor(value, context) {
|
|
121
|
+
if (isSignalRef(value)) {
|
|
122
|
+
return value.id;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === "string" && context.signals?.has?.(value)) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readSignalValue(value, context) {
|
|
131
|
+
if (isSignalRef(value)) {
|
|
132
|
+
return value.value;
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === "string" && context.signals?.has?.(value)) {
|
|
135
|
+
return context.signals.get(value);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isInlineBindingValue(value) {
|
|
141
|
+
return Boolean(value && typeof value === "object");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function registerInlineBinding(value, context) {
|
|
145
|
+
if (typeof context.bind !== "function") {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
return context.bind(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
58
151
|
export function escapeHtml(value) {
|
|
59
152
|
return String(value)
|
|
60
153
|
.replaceAll("&", "&")
|