@jxsuite/compiler 0.28.0 → 0.28.3
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/dist/cli.js +166 -249
- package/dist/compiler.js +100 -188
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +35 -29
- package/src/shared.ts +402 -231
- package/src/site/content-loader.ts +54 -29
- package/src/site/context-injection.ts +13 -9
- package/src/site/format-host.ts +26 -15
- package/src/site/head-merger.ts +37 -18
- package/src/site/image-cache.ts +13 -12
- package/src/site/image-optimizer.ts +32 -23
- package/src/site/image-transform.ts +172 -81
- package/src/site/layout-resolver.ts +33 -18
- package/src/site/pages-discovery.ts +61 -39
- package/src/site/prototype-resolver.ts +45 -21
- package/src/site/site-build.ts +232 -183
- package/src/site/site-loader.ts +71 -52
- package/src/targets/compile-class.ts +70 -51
- package/src/targets/compile-client.ts +271 -227
- package/src/targets/compile-element.ts +230 -146
- package/src/targets/compile-server.ts +16 -62
- package/src/targets/compile-static.ts +27 -19
- package/src/targets/compile-image-endpoint.ts +0 -138
package/src/shared.ts
CHANGED
|
@@ -6,17 +6,29 @@
|
|
|
6
6
|
* compilation targets (static, client, element, server).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { camelToKebab, toCSSText
|
|
10
|
-
import { compileExpression,
|
|
9
|
+
import { RESERVED_KEYS, camelToKebab, toCSSText } from "@jxsuite/runtime";
|
|
10
|
+
import { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
11
|
+
import {
|
|
12
|
+
isExpressionDef,
|
|
13
|
+
isFunctionDef,
|
|
14
|
+
isPrototypeDef,
|
|
15
|
+
isRef,
|
|
16
|
+
isSchemaOnlyDef as isSchemaOnly,
|
|
17
|
+
isServerFnDef,
|
|
18
|
+
isTemplateString,
|
|
19
|
+
paramNames,
|
|
20
|
+
} from "@jxsuite/schema/guards";
|
|
11
21
|
import type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
12
22
|
import type {
|
|
23
|
+
JsonValue,
|
|
13
24
|
JxElement,
|
|
25
|
+
JxMappedArray,
|
|
14
26
|
JxMutableNode,
|
|
15
|
-
JxStyle,
|
|
16
|
-
JxStateDefinition,
|
|
17
27
|
JxPrototypeDef,
|
|
28
|
+
JxRef,
|
|
29
|
+
JxStateDefinition,
|
|
18
30
|
JxStateObject,
|
|
19
|
-
|
|
31
|
+
JxStyle,
|
|
20
32
|
} from "@jxsuite/schema/types";
|
|
21
33
|
|
|
22
34
|
// Re-export runtime utilities used by submodules
|
|
@@ -34,30 +46,14 @@ export type { ExpressionNode };
|
|
|
34
46
|
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.32";
|
|
35
47
|
export const DEFAULT_LIT_HTML_SRC = "https://esm.sh/lit-html@3.3.0";
|
|
36
48
|
|
|
37
|
-
// ─── Schema keywords
|
|
49
|
+
// ─── Schema keywords & detection ─────────────────────────────────────────────
|
|
50
|
+
// Centralized in @jxsuite/schema/guards; re-exported here for existing callers.
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"type",
|
|
45
|
-
"enum",
|
|
46
|
-
"minimum",
|
|
47
|
-
"maximum",
|
|
48
|
-
"minLength",
|
|
49
|
-
"maxLength",
|
|
50
|
-
"pattern",
|
|
51
|
-
"format",
|
|
52
|
-
"items",
|
|
53
|
-
"properties",
|
|
54
|
-
"required",
|
|
55
|
-
"description",
|
|
56
|
-
"title",
|
|
57
|
-
"$comment",
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
52
|
+
export {
|
|
53
|
+
SCHEMA_KEYWORDS,
|
|
54
|
+
isSchemaOnlyDef as isSchemaOnly,
|
|
55
|
+
isTemplateString,
|
|
56
|
+
} from "@jxsuite/schema/guards";
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
59
|
* Returns true if a $src path points to a .class.json schema-defined class.
|
|
@@ -65,33 +61,10 @@ export const SCHEMA_KEYWORDS = new Set([
|
|
|
65
61
|
* @param {unknown} src
|
|
66
62
|
* @returns {boolean}
|
|
67
63
|
*/
|
|
68
|
-
export function isClassJsonSrc(src
|
|
64
|
+
export function isClassJsonSrc(src?: unknown): src is string {
|
|
69
65
|
return typeof src === "string" && src.endsWith(".class.json");
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
/**
|
|
73
|
-
* Returns true if an object contains only schema keywords (no `default`, no `$prototype`).
|
|
74
|
-
*
|
|
75
|
-
* @param {Record<string, unknown>} obj
|
|
76
|
-
* @returns {boolean}
|
|
77
|
-
*/
|
|
78
|
-
export function isSchemaOnly(obj: Record<string, unknown>) {
|
|
79
|
-
for (const k of Object.keys(obj)) {
|
|
80
|
-
if (!SCHEMA_KEYWORDS.has(k)) return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Returns true if a string contains a ${} template expression.
|
|
87
|
-
*
|
|
88
|
-
* @param {unknown} val
|
|
89
|
-
* @returns {boolean}
|
|
90
|
-
*/
|
|
91
|
-
export function isTemplateString(val: unknown) {
|
|
92
|
-
return typeof val === "string" && val.includes("${");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
68
|
/**
|
|
96
69
|
* Determine whether a node (or any of its descendants) requires client-side JavaScript.
|
|
97
70
|
*
|
|
@@ -99,55 +72,84 @@ export function isTemplateString(val: unknown) {
|
|
|
99
72
|
* @returns {boolean}
|
|
100
73
|
*/
|
|
101
74
|
export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
102
|
-
if (!def || typeof def !== "object")
|
|
75
|
+
if (!def || typeof def !== "object") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
103
78
|
|
|
104
79
|
if (def.state) {
|
|
105
80
|
for (const [k, d] of Object.entries(def.state)) {
|
|
106
81
|
// Skip injected context (read-only, not reactive)
|
|
107
|
-
if (k === "$site" || k === "$page")
|
|
82
|
+
if (k === "$site" || k === "$page") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
108
85
|
// Skip timing: "compiler" entries — resolved at build time, baked into static HTML
|
|
109
86
|
if (
|
|
110
87
|
d &&
|
|
111
88
|
typeof d === "object" &&
|
|
112
89
|
!Array.isArray(d) &&
|
|
113
90
|
(d as JxPrototypeDef).timing === "compiler"
|
|
114
|
-
)
|
|
91
|
+
) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (typeof d !== "object" || d === null || Array.isArray(d)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if ((d as JxPrototypeDef).$prototype) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if ("default" in /** @type {object} */ d) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (isSchemaOnly(d)) {
|
|
115
104
|
continue;
|
|
116
|
-
|
|
117
|
-
if ((d as JxPrototypeDef).$prototype) return true;
|
|
118
|
-
if ("default" in /** @type {object} */ d) return true;
|
|
119
|
-
if (isSchemaOnly(d)) continue;
|
|
105
|
+
}
|
|
120
106
|
return true;
|
|
121
107
|
}
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
if (def.$switch)
|
|
110
|
+
if (def.$switch) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
125
113
|
if (
|
|
126
114
|
!Array.isArray(def.children) &&
|
|
127
115
|
(def.children as JxMappedArray | undefined)?.$prototype === "Array"
|
|
128
|
-
)
|
|
116
|
+
) {
|
|
129
117
|
return true;
|
|
118
|
+
}
|
|
130
119
|
|
|
131
|
-
if (Array.isArray(def.children)) {
|
|
132
|
-
|
|
120
|
+
if (Array.isArray(def.children) && def.children.some((c) => isDynamic(c))) {
|
|
121
|
+
return true;
|
|
133
122
|
}
|
|
134
123
|
|
|
135
124
|
for (const [key, val] of Object.entries(def)) {
|
|
136
|
-
if (RESERVED_KEYS.has(key))
|
|
137
|
-
|
|
125
|
+
if (RESERVED_KEYS.has(key)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
val !== null &&
|
|
130
|
+
typeof val === "object" &&
|
|
131
|
+
typeof (val as JxMutableNode).$ref === "string"
|
|
132
|
+
) {
|
|
138
133
|
return true;
|
|
139
|
-
|
|
134
|
+
}
|
|
135
|
+
if (isTemplateString(val)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
if (def.style && typeof def.style === "object") {
|
|
143
141
|
for (const val of Object.values(def.style)) {
|
|
144
|
-
if (isTemplateString(val))
|
|
142
|
+
if (isTemplateString(val)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (def.attributes && typeof def.attributes === "object") {
|
|
149
149
|
for (const val of Object.values(def.attributes)) {
|
|
150
|
-
if (isTemplateString(val))
|
|
150
|
+
if (isTemplateString(val)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -161,31 +163,49 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
161
163
|
* @returns {boolean}
|
|
162
164
|
*/
|
|
163
165
|
export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
164
|
-
if (!def || typeof def !== "object")
|
|
166
|
+
if (!def || typeof def !== "object") {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
165
169
|
|
|
166
|
-
if (def.$switch)
|
|
170
|
+
if (def.$switch) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
167
173
|
if (
|
|
168
174
|
!Array.isArray(def.children) &&
|
|
169
175
|
(def.children as JxMappedArray | undefined)?.$prototype === "Array"
|
|
170
|
-
)
|
|
176
|
+
) {
|
|
171
177
|
return true;
|
|
178
|
+
}
|
|
172
179
|
|
|
173
180
|
for (const [key, val] of Object.entries(def)) {
|
|
174
|
-
if (RESERVED_KEYS.has(key))
|
|
175
|
-
|
|
181
|
+
if (RESERVED_KEYS.has(key)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
val !== null &&
|
|
186
|
+
typeof val === "object" &&
|
|
187
|
+
typeof (val as JxMutableNode).$ref === "string"
|
|
188
|
+
) {
|
|
176
189
|
return true;
|
|
177
|
-
|
|
190
|
+
}
|
|
191
|
+
if (isTemplateString(val)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
if (def.style && typeof def.style === "object") {
|
|
181
197
|
for (const val of Object.values(def.style)) {
|
|
182
|
-
if (isTemplateString(val))
|
|
198
|
+
if (isTemplateString(val)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
if (def.attributes && typeof def.attributes === "object") {
|
|
187
205
|
for (const val of Object.values(def.attributes)) {
|
|
188
|
-
if (isTemplateString(val))
|
|
206
|
+
if (isTemplateString(val)) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
211
|
|
|
@@ -199,9 +219,15 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
199
219
|
* @returns {boolean}
|
|
200
220
|
*/
|
|
201
221
|
export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
202
|
-
if (!def || typeof def !== "object")
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
if (!def || typeof def !== "object") {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (isDynamic(def)) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(def.children)) {
|
|
229
|
+
return def.children.some((c): boolean => hasAnyIsland(c));
|
|
230
|
+
}
|
|
205
231
|
return false;
|
|
206
232
|
}
|
|
207
233
|
|
|
@@ -212,22 +238,24 @@ export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
|
212
238
|
* @param {Record<string, unknown> | null} [parentScope]
|
|
213
239
|
* @param {Record<string, unknown>} [scopeDefs]
|
|
214
240
|
* @param {Record<string, string>} [media]
|
|
215
|
-
* @returns {
|
|
216
|
-
* scope: Record<string, unknown>;
|
|
217
|
-
* scopeDefs: Record<string, unknown>;
|
|
218
|
-
* media: Record<string, string>;
|
|
219
|
-
* }}
|
|
241
|
+
* @returns {CompileContext}
|
|
220
242
|
*/
|
|
243
|
+
export interface CompileContext {
|
|
244
|
+
scope: Record<string, unknown>;
|
|
245
|
+
scopeDefs: Record<string, unknown>;
|
|
246
|
+
media: Record<string, string>;
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
export function createCompileContext(
|
|
222
250
|
raw: JxElement | JxMutableNode | null,
|
|
223
251
|
parentScope: Record<string, unknown> | null = null,
|
|
224
252
|
scopeDefs: Record<string, unknown> = {},
|
|
225
253
|
media: Record<string, string> = {},
|
|
226
|
-
) {
|
|
254
|
+
): CompileContext {
|
|
227
255
|
const scope = raw?.state
|
|
228
256
|
? buildInitialScope(raw.state, parentScope)
|
|
229
257
|
: (parentScope ?? Object.create(null));
|
|
230
|
-
return { scope, scopeDefs
|
|
258
|
+
return { media, scope, scopeDefs };
|
|
231
259
|
}
|
|
232
260
|
|
|
233
261
|
/**
|
|
@@ -261,12 +289,13 @@ export function buildInitialScope(
|
|
|
261
289
|
defineLazyScopeValue(scope, key, () => evaluateStaticTemplate(def, scope));
|
|
262
290
|
continue;
|
|
263
291
|
}
|
|
264
|
-
if (!def || typeof def !== "object")
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
if (!def || typeof def !== "object") {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (isExpressionDef(def)) {
|
|
296
|
+
const node = def.$expression as ExpressionNode;
|
|
268
297
|
if (isMutating(node.operator)) {
|
|
269
|
-
setOwnScopeValue(scope, key, (s: Record<string,
|
|
298
|
+
setOwnScopeValue(scope, key, (s: Record<string, unknown>, event: Event) =>
|
|
270
299
|
evaluateExpression(node, s, event),
|
|
271
300
|
);
|
|
272
301
|
} else {
|
|
@@ -274,21 +303,25 @@ export function buildInitialScope(
|
|
|
274
303
|
}
|
|
275
304
|
continue;
|
|
276
305
|
}
|
|
277
|
-
if (
|
|
278
|
-
if (
|
|
279
|
-
const
|
|
280
|
-
|
|
306
|
+
if (isFunctionDef(def)) {
|
|
307
|
+
if (def.body) {
|
|
308
|
+
const names = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
309
|
+
const fn = new Function("state", ...names, def.body);
|
|
310
|
+
if (def.body.includes("return")) {
|
|
281
311
|
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
282
312
|
} else {
|
|
283
313
|
setOwnScopeValue(scope, key, fn);
|
|
284
314
|
}
|
|
285
|
-
} else
|
|
315
|
+
} else {
|
|
286
316
|
setOwnScopeValue(scope, key, () => {});
|
|
287
317
|
}
|
|
288
318
|
continue;
|
|
289
319
|
}
|
|
290
|
-
if (
|
|
291
|
-
|
|
320
|
+
if (
|
|
321
|
+
isPrototypeDef(def) &&
|
|
322
|
+
(def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage")
|
|
323
|
+
) {
|
|
324
|
+
setOwnScopeValue(scope, key, cloneValue(def.default ?? null));
|
|
292
325
|
}
|
|
293
326
|
}
|
|
294
327
|
|
|
@@ -302,9 +335,9 @@ export function buildInitialScope(
|
|
|
302
335
|
*/
|
|
303
336
|
export function setOwnScopeValue(scope: Record<string, unknown>, key: string, value: unknown) {
|
|
304
337
|
Object.defineProperty(scope, key, {
|
|
305
|
-
value,
|
|
306
|
-
enumerable: true,
|
|
307
338
|
configurable: true,
|
|
339
|
+
enumerable: true,
|
|
340
|
+
value,
|
|
308
341
|
writable: true,
|
|
309
342
|
});
|
|
310
343
|
}
|
|
@@ -320,8 +353,8 @@ export function defineLazyScopeValue(
|
|
|
320
353
|
getter: () => unknown,
|
|
321
354
|
) {
|
|
322
355
|
Object.defineProperty(scope, key, {
|
|
323
|
-
enumerable: true,
|
|
324
356
|
configurable: true,
|
|
357
|
+
enumerable: true,
|
|
325
358
|
get: getter,
|
|
326
359
|
});
|
|
327
360
|
}
|
|
@@ -332,8 +365,12 @@ export function defineLazyScopeValue(
|
|
|
332
365
|
* @returns {unknown}
|
|
333
366
|
*/
|
|
334
367
|
export function resolveStaticValue(value: unknown, scope: Record<string, unknown> | null) {
|
|
335
|
-
if (isRefObject(value))
|
|
336
|
-
|
|
368
|
+
if (isRefObject(value)) {
|
|
369
|
+
return resolveRefValue((value as JxMutableNode).$ref, scope!);
|
|
370
|
+
}
|
|
371
|
+
if (isTemplateString(value)) {
|
|
372
|
+
return evaluateStaticTemplate(value as string, scope!);
|
|
373
|
+
}
|
|
337
374
|
return value;
|
|
338
375
|
}
|
|
339
376
|
|
|
@@ -341,10 +378,8 @@ export function resolveStaticValue(value: unknown, scope: Record<string, unknown
|
|
|
341
378
|
* @param {unknown} value
|
|
342
379
|
* @returns {boolean}
|
|
343
380
|
*/
|
|
344
|
-
export function isRefObject(value: unknown) {
|
|
345
|
-
return (
|
|
346
|
-
value !== null && typeof value === "object" && typeof (value as JxMutableNode).$ref === "string"
|
|
347
|
-
);
|
|
381
|
+
export function isRefObject(value: unknown): value is JxRef {
|
|
382
|
+
return isRef(value);
|
|
348
383
|
}
|
|
349
384
|
|
|
350
385
|
/**
|
|
@@ -353,17 +388,21 @@ export function isRefObject(value: unknown) {
|
|
|
353
388
|
* @returns {unknown}
|
|
354
389
|
*/
|
|
355
390
|
export function resolveRefValue(refValue: unknown, scope: Record<string, unknown>) {
|
|
356
|
-
if (typeof refValue !== "string")
|
|
391
|
+
if (typeof refValue !== "string") {
|
|
392
|
+
return refValue;
|
|
393
|
+
}
|
|
357
394
|
if (refValue.startsWith("$map/")) {
|
|
358
395
|
const parts = refValue.split("/");
|
|
359
|
-
const key = parts
|
|
360
|
-
const base = (scope.$map as Record<string, unknown> | undefined)?.[key] ?? scope[
|
|
396
|
+
const [, key] = parts;
|
|
397
|
+
const base = (scope.$map as Record<string, unknown> | undefined)?.[key] ?? scope[`$map/${key}`];
|
|
361
398
|
return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
|
|
362
399
|
}
|
|
363
400
|
if (refValue.startsWith("#/state/")) {
|
|
364
401
|
const sub = refValue.slice("#/state/".length);
|
|
365
402
|
const slash = sub.indexOf("/");
|
|
366
|
-
if (slash
|
|
403
|
+
if (slash === -1) {
|
|
404
|
+
return scope[sub];
|
|
405
|
+
}
|
|
367
406
|
return getPathValue(scope[sub.slice(0, slash)], sub.slice(slash + 1));
|
|
368
407
|
}
|
|
369
408
|
return scope[refValue] ?? null;
|
|
@@ -394,13 +433,14 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
394
433
|
* @returns {unknown}
|
|
395
434
|
*/
|
|
396
435
|
export function getPathValue(base: unknown, path: string) {
|
|
397
|
-
if (!path)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
436
|
+
if (!path) {
|
|
437
|
+
return base;
|
|
438
|
+
}
|
|
439
|
+
let acc: unknown = base;
|
|
440
|
+
for (const key of path.split("/")) {
|
|
441
|
+
acc = acc == null ? undefined : (acc as Record<string, unknown>)[key];
|
|
442
|
+
}
|
|
443
|
+
return acc;
|
|
404
444
|
}
|
|
405
445
|
|
|
406
446
|
/**
|
|
@@ -408,8 +448,10 @@ export function getPathValue(base: unknown, path: string) {
|
|
|
408
448
|
* @returns {unknown}
|
|
409
449
|
*/
|
|
410
450
|
export function cloneValue(value: unknown) {
|
|
411
|
-
if (value === null || typeof value !== "object")
|
|
412
|
-
|
|
451
|
+
if (value === null || typeof value !== "object") {
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
return structuredClone(value);
|
|
413
455
|
}
|
|
414
456
|
|
|
415
457
|
// ─── HTML building ────────────────────────────────────────────────────────────
|
|
@@ -431,14 +473,27 @@ export function buildAttrs(def: JxElement | JxMutableNode, scope: Record<string,
|
|
|
431
473
|
const lang = resolveStaticValue(def.lang, scope);
|
|
432
474
|
const dir = resolveStaticValue(def.dir, scope);
|
|
433
475
|
|
|
434
|
-
if (id)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
476
|
+
if (id) {
|
|
477
|
+
out += ` id="${escapeHtml(String(id))}"`;
|
|
478
|
+
}
|
|
479
|
+
if (className) {
|
|
480
|
+
out += ` class="${escapeHtml(String(className))}"`;
|
|
481
|
+
}
|
|
482
|
+
if (hidden) {
|
|
483
|
+
out += " hidden";
|
|
484
|
+
}
|
|
485
|
+
if (tabIndex !== undefined && tabIndex !== null) {
|
|
438
486
|
out += ` tabindex="${escapeHtml(String(tabIndex))}"`;
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
|
|
487
|
+
}
|
|
488
|
+
if (title) {
|
|
489
|
+
out += ` title="${escapeHtml(String(title))}"`;
|
|
490
|
+
}
|
|
491
|
+
if (lang) {
|
|
492
|
+
out += ` lang="${escapeHtml(String(lang))}"`;
|
|
493
|
+
}
|
|
494
|
+
if (dir) {
|
|
495
|
+
out += ` dir="${escapeHtml(String(dir))}"`;
|
|
496
|
+
}
|
|
442
497
|
|
|
443
498
|
if (def.style && scope) {
|
|
444
499
|
const inline = Object.entries(def.style)
|
|
@@ -460,7 +515,9 @@ export function buildAttrs(def: JxElement | JxMutableNode, scope: Record<string,
|
|
|
460
515
|
})
|
|
461
516
|
.filter(Boolean)
|
|
462
517
|
.join("; ");
|
|
463
|
-
if (inline)
|
|
518
|
+
if (inline) {
|
|
519
|
+
out += ` style="${inline}"`;
|
|
520
|
+
}
|
|
464
521
|
}
|
|
465
522
|
if (def.attributes) {
|
|
466
523
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
@@ -476,8 +533,12 @@ export function buildAttrs(def: JxElement | JxMutableNode, scope: Record<string,
|
|
|
476
533
|
}
|
|
477
534
|
|
|
478
535
|
if (def.tagName === "img") {
|
|
479
|
-
if (!def.attributes?.loading)
|
|
480
|
-
|
|
536
|
+
if (!def.attributes?.loading) {
|
|
537
|
+
out += ` loading="lazy"`;
|
|
538
|
+
}
|
|
539
|
+
if (!def.attributes?.decoding) {
|
|
540
|
+
out += ` decoding="async"`;
|
|
541
|
+
}
|
|
481
542
|
}
|
|
482
543
|
|
|
483
544
|
if (def.$static) {
|
|
@@ -499,7 +560,7 @@ export function buildAttrs(def: JxElement | JxMutableNode, scope: Record<string,
|
|
|
499
560
|
* scopeDefs: Record<string, unknown>;
|
|
500
561
|
* media: Record<string, string>;
|
|
501
562
|
* }} context
|
|
502
|
-
* @param {(def:
|
|
563
|
+
* @param {(def: unknown, raw: unknown, context: unknown) => string} childCompiler
|
|
503
564
|
* @returns {string}
|
|
504
565
|
*/
|
|
505
566
|
export function buildInner(
|
|
@@ -518,8 +579,9 @@ export function buildInner(
|
|
|
518
579
|
const value = resolveStaticValue(source.textContent, context.scope);
|
|
519
580
|
return value == null ? "" : escapeHtml(String(value));
|
|
520
581
|
}
|
|
521
|
-
if (source.innerHTML)
|
|
582
|
+
if (source.innerHTML) {
|
|
522
583
|
return (resolveStaticValue(source.innerHTML, context.scope) as string) ?? "";
|
|
584
|
+
}
|
|
523
585
|
if (Array.isArray(source.children)) {
|
|
524
586
|
const rawChildren = raw?.children;
|
|
525
587
|
return source.children
|
|
@@ -550,30 +612,42 @@ export function compileStyles(
|
|
|
550
612
|
const rules: string[] = [];
|
|
551
613
|
|
|
552
614
|
// Emit project-level (site-wide) styles — CSS custom properties go on :root,
|
|
553
|
-
//
|
|
554
|
-
//
|
|
615
|
+
// Everything else on body. Project-level style is implicitly :root, so a
|
|
616
|
+
// Flat object like { "--bg": "#000", "margin": "0" } is the expected format.
|
|
555
617
|
if (projectStyle && typeof projectStyle === "object") {
|
|
556
|
-
|
|
618
|
+
const emitProjectRules = (selector: string, obj: Record<string, unknown>) => {
|
|
557
619
|
const props = toCSSText(obj);
|
|
558
|
-
if (props)
|
|
620
|
+
if (props) {
|
|
621
|
+
rules.push(`${selector} { ${props} }`);
|
|
622
|
+
}
|
|
559
623
|
for (const [key, val] of Object.entries(obj)) {
|
|
560
|
-
if (val === null || typeof val !== "object" || Array.isArray(val))
|
|
624
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
561
627
|
if (key.startsWith("@")) {
|
|
562
628
|
const atRule = key.startsWith("@--")
|
|
563
629
|
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
564
630
|
: key;
|
|
565
631
|
const mProps = toCSSText(val);
|
|
566
|
-
if (mProps)
|
|
632
|
+
if (mProps) {
|
|
633
|
+
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
634
|
+
}
|
|
567
635
|
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
|
|
568
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub))
|
|
569
|
-
|
|
636
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (sel.startsWith("@")) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
570
642
|
const resolved = sel.startsWith("&")
|
|
571
643
|
? sel.replace("&", selector)
|
|
572
644
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
573
645
|
? `${selector}${sel}`
|
|
574
646
|
: `${selector} ${sel}`;
|
|
575
647
|
const subProps = toCSSText(sub);
|
|
576
|
-
if (subProps)
|
|
648
|
+
if (subProps) {
|
|
649
|
+
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
650
|
+
}
|
|
577
651
|
}
|
|
578
652
|
continue;
|
|
579
653
|
}
|
|
@@ -584,7 +658,7 @@ export function compileStyles(
|
|
|
584
658
|
: `${selector} ${key}`;
|
|
585
659
|
emitProjectRules(resolved, val as Record<string, unknown>);
|
|
586
660
|
}
|
|
587
|
-
}
|
|
661
|
+
};
|
|
588
662
|
|
|
589
663
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
590
664
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
@@ -612,9 +686,17 @@ export function compileStyles(
|
|
|
612
686
|
// Collect direct CSS properties into body {}
|
|
613
687
|
const bodyProps: Record<string, unknown> = {};
|
|
614
688
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
615
|
-
if (
|
|
689
|
+
if (
|
|
690
|
+
key.startsWith(":") ||
|
|
691
|
+
key.startsWith(".") ||
|
|
692
|
+
key.startsWith("[") ||
|
|
693
|
+
key.startsWith("@")
|
|
694
|
+
) {
|
|
616
695
|
continue;
|
|
617
|
-
|
|
696
|
+
}
|
|
697
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
618
700
|
if (key.startsWith("--")) {
|
|
619
701
|
rootProps[key] = val;
|
|
620
702
|
} else {
|
|
@@ -633,7 +715,9 @@ export function compileStyles(
|
|
|
633
715
|
|
|
634
716
|
const counter = { n: 0 };
|
|
635
717
|
collectStyles(doc, rules, mediaQueries, "", counter);
|
|
636
|
-
if (rules.length === 0)
|
|
718
|
+
if (rules.length === 0) {
|
|
719
|
+
return "";
|
|
720
|
+
}
|
|
637
721
|
return `<style>\n${rules.join("\n")}\n</style>`;
|
|
638
722
|
}
|
|
639
723
|
|
|
@@ -652,9 +736,13 @@ function emitNestedElement(
|
|
|
652
736
|
mediaQueries: Record<string, string>,
|
|
653
737
|
) {
|
|
654
738
|
const props = toCSSText(obj);
|
|
655
|
-
if (props)
|
|
739
|
+
if (props) {
|
|
740
|
+
rules.push(`${selector} { ${props} }`);
|
|
741
|
+
}
|
|
656
742
|
for (const [key, val] of Object.entries(obj)) {
|
|
657
|
-
if (val === null || typeof val !== "object" || Array.isArray(val))
|
|
743
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
658
746
|
if (key.startsWith("@")) {
|
|
659
747
|
const atRule = key.startsWith("@--")
|
|
660
748
|
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
@@ -662,17 +750,25 @@ function emitNestedElement(
|
|
|
662
750
|
? `@media ${key.slice(1)}`
|
|
663
751
|
: key;
|
|
664
752
|
const mProps = toCSSText(val);
|
|
665
|
-
if (mProps)
|
|
753
|
+
if (mProps) {
|
|
754
|
+
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
755
|
+
}
|
|
666
756
|
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
|
|
667
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub))
|
|
668
|
-
|
|
757
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (sel.startsWith("@")) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
669
763
|
const resolved = sel.startsWith("&")
|
|
670
764
|
? sel.replace("&", selector)
|
|
671
765
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
672
766
|
? `${selector}${sel}`
|
|
673
767
|
: `${selector} ${sel}`;
|
|
674
768
|
const subProps = toCSSText(sub);
|
|
675
|
-
if (subProps)
|
|
769
|
+
if (subProps) {
|
|
770
|
+
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
771
|
+
}
|
|
676
772
|
}
|
|
677
773
|
continue;
|
|
678
774
|
}
|
|
@@ -696,16 +792,18 @@ export function collectStyles(
|
|
|
696
792
|
def: JxElement | JxMutableNode | string,
|
|
697
793
|
rules: string[],
|
|
698
794
|
mediaQueries: Record<string, string>,
|
|
699
|
-
_parentSel
|
|
700
|
-
|
|
795
|
+
_parentSel = "",
|
|
796
|
+
counterArg?: { n: number },
|
|
701
797
|
prefix = "jx",
|
|
702
798
|
) {
|
|
703
|
-
|
|
799
|
+
const counter = counterArg ?? { n: 0 };
|
|
800
|
+
if (!def || typeof def !== "object") {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
704
803
|
|
|
705
|
-
if (def.style) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
804
|
+
if (def.style && !def.id && !def.className) {
|
|
805
|
+
def.className = `${prefix}-${counter.n}`;
|
|
806
|
+
counter.n += 1;
|
|
709
807
|
}
|
|
710
808
|
|
|
711
809
|
const selector = def.id
|
|
@@ -723,10 +821,15 @@ export function collectStyles(
|
|
|
723
821
|
prop.startsWith("&") ||
|
|
724
822
|
prop.startsWith("[") ||
|
|
725
823
|
prop.startsWith("@")
|
|
726
|
-
)
|
|
824
|
+
) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (value === null || typeof value === "object") {
|
|
727
828
|
continue;
|
|
728
|
-
|
|
729
|
-
if (typeof value === "string" && isTemplateString(value))
|
|
829
|
+
}
|
|
830
|
+
if (typeof value === "string" && isTemplateString(value)) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
730
833
|
baseDecls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
731
834
|
}
|
|
732
835
|
if (baseDecls.length > 0) {
|
|
@@ -734,7 +837,9 @@ export function collectStyles(
|
|
|
734
837
|
}
|
|
735
838
|
|
|
736
839
|
for (const [prop, val] of Object.entries(def.style)) {
|
|
737
|
-
if (val === null || typeof val !== "object" || Array.isArray(val))
|
|
840
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
738
843
|
if (prop.startsWith("@")) {
|
|
739
844
|
const atRule = prop.startsWith("@--")
|
|
740
845
|
? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
|
|
@@ -742,13 +847,22 @@ export function collectStyles(
|
|
|
742
847
|
? `@media ${prop.slice(1)}`
|
|
743
848
|
: prop;
|
|
744
849
|
const atBaseProps = toCSSText(val);
|
|
745
|
-
if (atBaseProps)
|
|
850
|
+
if (atBaseProps) {
|
|
851
|
+
rules.push(`${atRule} { ${selector} { ${atBaseProps} } }`);
|
|
852
|
+
}
|
|
746
853
|
for (const [sel, nestedRules] of Object.entries(
|
|
747
854
|
/** @type {Record<string, unknown>} */ val,
|
|
748
855
|
)) {
|
|
749
|
-
if (
|
|
856
|
+
if (
|
|
857
|
+
nestedRules === null ||
|
|
858
|
+
typeof nestedRules !== "object" ||
|
|
859
|
+
Array.isArray(nestedRules)
|
|
860
|
+
) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (sel.startsWith("@")) {
|
|
750
864
|
continue;
|
|
751
|
-
|
|
865
|
+
}
|
|
752
866
|
const resolved = sel.startsWith("&")
|
|
753
867
|
? sel.replace("&", selector)
|
|
754
868
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
@@ -773,9 +887,23 @@ export function collectStyles(
|
|
|
773
887
|
}
|
|
774
888
|
|
|
775
889
|
if (Array.isArray(def.children)) {
|
|
776
|
-
|
|
890
|
+
for (const c of def.children) {
|
|
777
891
|
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
778
|
-
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// $switch case nodes render in place of the container's content — collect their styles too
|
|
896
|
+
if (def.cases && typeof def.cases === "object") {
|
|
897
|
+
for (const c of Object.values(def.cases)) {
|
|
898
|
+
collectStyles(
|
|
899
|
+
c as JxElement | JxMutableNode | string,
|
|
900
|
+
rules,
|
|
901
|
+
mediaQueries,
|
|
902
|
+
selector,
|
|
903
|
+
counter,
|
|
904
|
+
prefix,
|
|
905
|
+
);
|
|
906
|
+
}
|
|
779
907
|
}
|
|
780
908
|
}
|
|
781
909
|
|
|
@@ -789,11 +917,11 @@ export function collectStyles(
|
|
|
789
917
|
*/
|
|
790
918
|
export function escapeHtml(str: string) {
|
|
791
919
|
return String(str)
|
|
792
|
-
.
|
|
793
|
-
.
|
|
794
|
-
.
|
|
795
|
-
.
|
|
796
|
-
.
|
|
920
|
+
.replaceAll("&", "&")
|
|
921
|
+
.replaceAll("<", "<")
|
|
922
|
+
.replaceAll(">", ">")
|
|
923
|
+
.replaceAll('"', """)
|
|
924
|
+
.replaceAll("'", "'");
|
|
797
925
|
}
|
|
798
926
|
|
|
799
927
|
/**
|
|
@@ -805,8 +933,8 @@ export function escapeHtml(str: string) {
|
|
|
805
933
|
export function titleToTagName(title: string) {
|
|
806
934
|
const slug = title
|
|
807
935
|
.toLowerCase()
|
|
808
|
-
.
|
|
809
|
-
.
|
|
936
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
937
|
+
.replaceAll(/^-|-$/g, "");
|
|
810
938
|
return slug.includes("-") ? slug : `jx-${slug}`;
|
|
811
939
|
}
|
|
812
940
|
|
|
@@ -828,7 +956,7 @@ export function tagNameToClassName(tagName: string) {
|
|
|
828
956
|
* @returns {string[]}
|
|
829
957
|
*/
|
|
830
958
|
export function collectSrcImports(doc: JxElement) {
|
|
831
|
-
const srcs
|
|
959
|
+
const srcs = new Set<string>();
|
|
832
960
|
_walkSrc(doc, srcs);
|
|
833
961
|
return [...srcs];
|
|
834
962
|
}
|
|
@@ -838,7 +966,9 @@ export function collectSrcImports(doc: JxElement) {
|
|
|
838
966
|
* @param {Set<string>} srcs
|
|
839
967
|
*/
|
|
840
968
|
function _walkSrc(def: JxElement | JxMutableNode | string, srcs: Set<string>) {
|
|
841
|
-
if (!def || typeof def !== "object")
|
|
969
|
+
if (!def || typeof def !== "object") {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
842
972
|
if (def.state) {
|
|
843
973
|
for (const d of Object.values(def.state)) {
|
|
844
974
|
if (
|
|
@@ -852,7 +982,9 @@ function _walkSrc(def: JxElement | JxMutableNode | string, srcs: Set<string>) {
|
|
|
852
982
|
}
|
|
853
983
|
}
|
|
854
984
|
if (Array.isArray(def.children)) {
|
|
855
|
-
|
|
985
|
+
for (const c of def.children) {
|
|
986
|
+
_walkSrc(c, srcs);
|
|
987
|
+
}
|
|
856
988
|
}
|
|
857
989
|
}
|
|
858
990
|
|
|
@@ -877,34 +1009,26 @@ function _walkServerEntries(
|
|
|
877
1009
|
def: JxElement | JxMutableNode | string,
|
|
878
1010
|
entries: Map<string, { key: string; exportName: string; src: string }>,
|
|
879
1011
|
) {
|
|
880
|
-
if (!def || typeof def !== "object")
|
|
1012
|
+
if (!def || typeof def !== "object") {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
881
1015
|
if (def.state) {
|
|
882
1016
|
for (const [key, d] of Object.entries(def.state)) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
entry &&
|
|
886
|
-
typeof entry === "object" &&
|
|
887
|
-
entry.timing === "server" &&
|
|
888
|
-
entry.$src &&
|
|
889
|
-
entry.$export &&
|
|
890
|
-
!entry.$prototype
|
|
891
|
-
) {
|
|
892
|
-
entries.set(entry.$export, {
|
|
893
|
-
key,
|
|
894
|
-
exportName: entry.$export,
|
|
895
|
-
src: entry.$src,
|
|
896
|
-
});
|
|
1017
|
+
if (isServerFnDef(d)) {
|
|
1018
|
+
entries.set(d.$export, { exportName: d.$export, key, src: d.$src });
|
|
897
1019
|
}
|
|
898
1020
|
}
|
|
899
1021
|
}
|
|
900
1022
|
if (Array.isArray(def.children)) {
|
|
901
|
-
|
|
1023
|
+
for (const c of def.children) {
|
|
1024
|
+
_walkServerEntries(c, entries);
|
|
1025
|
+
}
|
|
902
1026
|
}
|
|
903
1027
|
}
|
|
904
1028
|
|
|
905
1029
|
// ─── Component pre-rendering ─────────────────────────────────────────────────
|
|
906
1030
|
|
|
907
|
-
const SELF_CLOSING
|
|
1031
|
+
const SELF_CLOSING = new Set<string>([
|
|
908
1032
|
"input",
|
|
909
1033
|
"br",
|
|
910
1034
|
"hr",
|
|
@@ -936,26 +1060,55 @@ export function renderStaticNode(
|
|
|
936
1060
|
}
|
|
937
1061
|
return escapeHtml(node);
|
|
938
1062
|
}
|
|
939
|
-
if (typeof node === "number" || typeof node === "boolean")
|
|
940
|
-
|
|
1063
|
+
if (typeof node === "number" || typeof node === "boolean") {
|
|
1064
|
+
return escapeHtml(String(node));
|
|
1065
|
+
}
|
|
1066
|
+
if (Array.isArray(node)) {
|
|
941
1067
|
return (node as (JxElement | JxMutableNode | string)[])
|
|
942
1068
|
.map((c: JxElement | JxMutableNode | string): string =>
|
|
943
1069
|
renderStaticNode(c, scope, slotContent),
|
|
944
1070
|
)
|
|
945
1071
|
.join("\n");
|
|
946
|
-
|
|
1072
|
+
}
|
|
1073
|
+
if (!node || typeof node !== "object") {
|
|
1074
|
+
return "";
|
|
1075
|
+
}
|
|
947
1076
|
|
|
948
1077
|
// Skip mapped arrays — can't pre-render dynamic lists
|
|
949
|
-
if (node.$prototype === "Array")
|
|
1078
|
+
if (node.$prototype === "Array") {
|
|
1079
|
+
return "";
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// $switch/cases — resolve the key statically and render the matched case inside a container
|
|
1083
|
+
// Element (mirrors the runtime's renderSwitch DOM shape). External $ref cases can't be fetched
|
|
1084
|
+
// At compile time, so they render the empty container.
|
|
1085
|
+
if (node.$switch) {
|
|
1086
|
+
const switchTag = node.tagName ?? "div";
|
|
1087
|
+
const attrs = buildAttrs(node, scope);
|
|
1088
|
+
const key = resolveStaticValue(node.$switch, scope);
|
|
1089
|
+
const caseDef =
|
|
1090
|
+
key == null
|
|
1091
|
+
? undefined
|
|
1092
|
+
: (node.cases as Record<string, JxElement | string> | undefined)?.[String(key)];
|
|
1093
|
+
const inner =
|
|
1094
|
+
caseDef !== undefined && !isRefObject(caseDef)
|
|
1095
|
+
? renderStaticNode(caseDef, scope, slotContent)
|
|
1096
|
+
: "";
|
|
1097
|
+
return `<${switchTag}${attrs}>${inner}</${switchTag}>`;
|
|
1098
|
+
}
|
|
950
1099
|
|
|
951
1100
|
const tag = node.tagName ?? "div";
|
|
952
1101
|
|
|
953
1102
|
// Replace <slot> with provided slot content
|
|
954
|
-
if (tag === "slot" && slotContent != null)
|
|
1103
|
+
if (tag === "slot" && slotContent != null) {
|
|
1104
|
+
return slotContent;
|
|
1105
|
+
}
|
|
955
1106
|
|
|
956
1107
|
const attrs = buildAttrs(node, scope);
|
|
957
1108
|
|
|
958
|
-
if (SELF_CLOSING.has(tag))
|
|
1109
|
+
if (SELF_CLOSING.has(tag)) {
|
|
1110
|
+
return `<${tag}${attrs}>`;
|
|
1111
|
+
}
|
|
959
1112
|
|
|
960
1113
|
let inner = "";
|
|
961
1114
|
if (node.textContent !== undefined) {
|
|
@@ -977,14 +1130,14 @@ export function renderStaticNode(
|
|
|
977
1130
|
* Pre-render a component definition to static HTML for its inner content.
|
|
978
1131
|
*
|
|
979
1132
|
* @param {JxElement} doc - Component JSON definition
|
|
980
|
-
* @param {Record<string,
|
|
981
|
-
* into state
|
|
1133
|
+
* @param {Record<string, JsonValue> | null} [propsOverride] - Instance-specific prop values to
|
|
1134
|
+
* merge into state
|
|
982
1135
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
983
1136
|
* @returns {string} The pre-rendered innerHTML
|
|
984
1137
|
*/
|
|
985
1138
|
export function preRenderComponentHtml(
|
|
986
1139
|
doc: JxElement,
|
|
987
|
-
propsOverride: Record<string,
|
|
1140
|
+
propsOverride: Record<string, JsonValue> | null = null,
|
|
988
1141
|
slotContent: string | null = null,
|
|
989
1142
|
) {
|
|
990
1143
|
let stateDefs: Record<string, JxStateDefinition> = doc.state ?? {};
|
|
@@ -993,26 +1146,22 @@ export function preRenderComponentHtml(
|
|
|
993
1146
|
for (const [key, value] of Object.entries(propsOverride)) {
|
|
994
1147
|
if (key in stateDefs) {
|
|
995
1148
|
const existing = stateDefs[key];
|
|
996
|
-
|
|
1149
|
+
stateDefs[key] =
|
|
997
1150
|
existing &&
|
|
998
1151
|
typeof existing === "object" &&
|
|
999
1152
|
!Array.isArray(existing) &&
|
|
1000
1153
|
"default" in existing
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
.../** @type {JxStateObject} */ existing,
|
|
1004
|
-
default: value,
|
|
1005
|
-
};
|
|
1006
|
-
} else {
|
|
1007
|
-
stateDefs[key] = value as JxStateDefinition;
|
|
1008
|
-
}
|
|
1154
|
+
? { .../** @type {JxStateObject} */ existing, default: value }
|
|
1155
|
+
: (value as JxStateDefinition);
|
|
1009
1156
|
} else {
|
|
1010
1157
|
stateDefs[key] = value as JxStateDefinition;
|
|
1011
1158
|
}
|
|
1012
1159
|
}
|
|
1013
1160
|
}
|
|
1014
1161
|
const scope = buildInitialScope(stateDefs, null);
|
|
1015
|
-
if (!Array.isArray(doc.children))
|
|
1162
|
+
if (!Array.isArray(doc.children)) {
|
|
1163
|
+
return "";
|
|
1164
|
+
}
|
|
1016
1165
|
return doc.children
|
|
1017
1166
|
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slotContent))
|
|
1018
1167
|
.join("\n");
|
|
@@ -1035,36 +1184,53 @@ export function isComponentFullyStatic(doc: JxElement) {
|
|
|
1035
1184
|
* @param {JxElement | string | (JxElement | string)[]} node
|
|
1036
1185
|
* @returns {boolean}
|
|
1037
1186
|
*/
|
|
1038
|
-
function _isStaticNode(node: JxElement | string | (JxElement | string)[]) {
|
|
1039
|
-
if (!node || typeof node !== "object")
|
|
1040
|
-
|
|
1187
|
+
function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boolean {
|
|
1188
|
+
if (!node || typeof node !== "object") {
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
if (Array.isArray(node)) {
|
|
1192
|
+
return node.every((n) => _isStaticNode(n));
|
|
1193
|
+
}
|
|
1041
1194
|
|
|
1042
1195
|
// Check for $prototype (Functions, Request, Storage, etc.)
|
|
1043
|
-
if (node.$prototype)
|
|
1196
|
+
if (node.$prototype) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1044
1199
|
// Check for $ref
|
|
1045
|
-
if (node.$ref)
|
|
1200
|
+
if (node.$ref) {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1046
1203
|
|
|
1047
1204
|
// Check state entries
|
|
1048
1205
|
if (node.state) {
|
|
1049
1206
|
for (const def of Object.values(node.state)) {
|
|
1050
|
-
if (!def || typeof def !== "object")
|
|
1207
|
+
if (!def || typeof def !== "object") {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1051
1210
|
const d = def as JxMutableNode;
|
|
1052
|
-
if (d.$prototype)
|
|
1053
|
-
|
|
1211
|
+
if (d.$prototype) {
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
if (d.$ref) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1054
1217
|
}
|
|
1055
1218
|
}
|
|
1056
1219
|
|
|
1057
1220
|
// Check for event handlers
|
|
1058
1221
|
for (const key of Object.keys(node)) {
|
|
1059
|
-
if (key.startsWith("on") && key !== "observedAttributes")
|
|
1222
|
+
if (key.startsWith("on") && key !== "observedAttributes") {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1060
1225
|
}
|
|
1061
1226
|
|
|
1062
1227
|
// Recurse into children
|
|
1063
1228
|
if (Array.isArray(node.children)) {
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1229
|
+
return node.children.every((c) => _isStaticNode(c));
|
|
1230
|
+
}
|
|
1231
|
+
// Children descriptor object ($prototype: "Array", etc.)
|
|
1232
|
+
if (node.children && typeof node.children === "object" && node.children.$prototype) {
|
|
1233
|
+
return false;
|
|
1068
1234
|
}
|
|
1069
1235
|
|
|
1070
1236
|
return true;
|
|
@@ -1082,7 +1248,7 @@ function _isStaticNode(node: JxElement | string | (JxElement | string)[]) {
|
|
|
1082
1248
|
*/
|
|
1083
1249
|
export function buildComponentCSS(
|
|
1084
1250
|
tagName: string,
|
|
1085
|
-
styleDef
|
|
1251
|
+
styleDef?: JxStyle | null | undefined,
|
|
1086
1252
|
doc: JxElement | null = null,
|
|
1087
1253
|
mediaQueries: Record<string, string> = {},
|
|
1088
1254
|
) {
|
|
@@ -1097,10 +1263,15 @@ export function buildComponentCSS(
|
|
|
1097
1263
|
prop.startsWith("&") ||
|
|
1098
1264
|
prop.startsWith("[") ||
|
|
1099
1265
|
prop.startsWith("@")
|
|
1100
|
-
)
|
|
1266
|
+
) {
|
|
1101
1267
|
continue;
|
|
1102
|
-
|
|
1103
|
-
if (typeof value === "
|
|
1268
|
+
}
|
|
1269
|
+
if (value === null || typeof value === "object") {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
if (typeof value === "string" && isTemplateString(value)) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1104
1275
|
decls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
1105
1276
|
}
|
|
1106
1277
|
if (decls.length > 0) {
|
|
@@ -1134,5 +1305,5 @@ export function buildComponentCSS(
|
|
|
1134
1305
|
}
|
|
1135
1306
|
}
|
|
1136
1307
|
|
|
1137
|
-
return rules.length > 0 ? rules.join("\n")
|
|
1308
|
+
return rules.length > 0 ? `${rules.join("\n")}\n` : "";
|
|
1138
1309
|
}
|