@jxsuite/compiler 0.19.0 → 0.21.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/dist/compiler.js +4 -4
- package/package.json +6 -4
- package/src/cli.js +1 -1
- package/src/compiler.js +2 -2
- package/src/shared.js +183 -145
- package/src/site/content-loader.js +106 -76
- package/src/site/context-injection.js +30 -24
- package/src/site/head-merger.js +13 -12
- package/src/site/image-optimizer.js +2 -2
- package/src/site/image-transform.js +5 -4
- package/src/site/layout-resolver.js +32 -25
- package/src/site/pages-discovery.js +6 -6
- package/src/site/prototype-resolver.js +1 -1
- package/src/site/site-build.js +71 -51
- package/src/site/site-loader.js +1 -1
- package/src/targets/compile-class.js +27 -21
- package/src/targets/compile-client.js +75 -33
- package/src/targets/compile-element.js +57 -39
- package/src/targets/compile-markdown.js +99 -78
- package/src/targets/compile-server.js +1 -1
- package/src/targets/compile-static.js +24 -13
package/src/shared.js
CHANGED
|
@@ -42,7 +42,7 @@ export const SCHEMA_KEYWORDS = new Set([
|
|
|
42
42
|
/**
|
|
43
43
|
* Returns true if a $src path points to a .class.json schema-defined class.
|
|
44
44
|
*
|
|
45
|
-
* @param {
|
|
45
|
+
* @param {unknown} src
|
|
46
46
|
* @returns {boolean}
|
|
47
47
|
*/
|
|
48
48
|
export function isClassJsonSrc(src) {
|
|
@@ -52,7 +52,7 @@ export function isClassJsonSrc(src) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Returns true if an object contains only schema keywords (no `default`, no `$prototype`).
|
|
54
54
|
*
|
|
55
|
-
* @param {
|
|
55
|
+
* @param {Record<string, unknown>} obj
|
|
56
56
|
* @returns {boolean}
|
|
57
57
|
*/
|
|
58
58
|
export function isSchemaOnly(obj) {
|
|
@@ -65,7 +65,7 @@ export function isSchemaOnly(obj) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Returns true if a string contains a ${} template expression.
|
|
67
67
|
*
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {unknown} val
|
|
69
69
|
* @returns {boolean}
|
|
70
70
|
*/
|
|
71
71
|
export function isTemplateString(val) {
|
|
@@ -75,7 +75,7 @@ export function isTemplateString(val) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Determine whether a node (or any of its descendants) requires client-side JavaScript.
|
|
77
77
|
*
|
|
78
|
-
* @param {
|
|
78
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
79
79
|
* @returns {boolean}
|
|
80
80
|
*/
|
|
81
81
|
export function isDynamic(def) {
|
|
@@ -90,22 +90,26 @@ export function isDynamic(def) {
|
|
|
90
90
|
d &&
|
|
91
91
|
typeof d === "object" &&
|
|
92
92
|
!Array.isArray(d) &&
|
|
93
|
-
/** @type {
|
|
93
|
+
/** @type {JxPrototypeDef} */ (d).timing === "compiler"
|
|
94
94
|
)
|
|
95
95
|
continue;
|
|
96
96
|
if (typeof d !== "object" || d === null || Array.isArray(d)) return true;
|
|
97
|
-
if (/** @type {
|
|
98
|
-
if ("default" in /** @type {
|
|
97
|
+
if (/** @type {JxPrototypeDef} */ (d).$prototype) return true;
|
|
98
|
+
if ("default" in /** @type {object} */ (d)) return true;
|
|
99
99
|
if (isSchemaOnly(d)) continue;
|
|
100
100
|
return true;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (def.$switch) return true;
|
|
105
|
-
if (
|
|
105
|
+
if (
|
|
106
|
+
!Array.isArray(def.children) &&
|
|
107
|
+
/** @type {JxMappedArray | undefined} */ (def.children)?.$prototype === "Array"
|
|
108
|
+
)
|
|
109
|
+
return true;
|
|
106
110
|
|
|
107
111
|
if (Array.isArray(def.children)) {
|
|
108
|
-
if (def.children.some(
|
|
112
|
+
if (def.children.some((c) => isDynamic(c))) return true;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
for (const [key, val] of Object.entries(def)) {
|
|
@@ -113,7 +117,7 @@ export function isDynamic(def) {
|
|
|
113
117
|
if (
|
|
114
118
|
val !== null &&
|
|
115
119
|
typeof val === "object" &&
|
|
116
|
-
typeof (/** @type {
|
|
120
|
+
typeof (/** @type {JxMutableNode} */ (val).$ref) === "string"
|
|
117
121
|
)
|
|
118
122
|
return true;
|
|
119
123
|
if (isTemplateString(val)) return true;
|
|
@@ -137,21 +141,25 @@ export function isDynamic(def) {
|
|
|
137
141
|
/**
|
|
138
142
|
* Shallow variant of isDynamic — checks only this node's own properties, not its children.
|
|
139
143
|
*
|
|
140
|
-
* @param {
|
|
144
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
141
145
|
* @returns {boolean}
|
|
142
146
|
*/
|
|
143
147
|
export function isNodeDynamic(def) {
|
|
144
148
|
if (!def || typeof def !== "object") return false;
|
|
145
149
|
|
|
146
150
|
if (def.$switch) return true;
|
|
147
|
-
if (
|
|
151
|
+
if (
|
|
152
|
+
!Array.isArray(def.children) &&
|
|
153
|
+
/** @type {JxMappedArray | undefined} */ (def.children)?.$prototype === "Array"
|
|
154
|
+
)
|
|
155
|
+
return true;
|
|
148
156
|
|
|
149
157
|
for (const [key, val] of Object.entries(def)) {
|
|
150
158
|
if (RESERVED_KEYS.has(key)) continue;
|
|
151
159
|
if (
|
|
152
160
|
val !== null &&
|
|
153
161
|
typeof val === "object" &&
|
|
154
|
-
typeof (/** @type {
|
|
162
|
+
typeof (/** @type {JxMutableNode} */ (val).$ref) === "string"
|
|
155
163
|
)
|
|
156
164
|
return true;
|
|
157
165
|
if (isTemplateString(val)) return true;
|
|
@@ -175,25 +183,28 @@ export function isNodeDynamic(def) {
|
|
|
175
183
|
/**
|
|
176
184
|
* Returns true if any node in the tree will need dynamic handling.
|
|
177
185
|
*
|
|
178
|
-
* @param {
|
|
186
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
179
187
|
* @returns {boolean}
|
|
180
188
|
*/
|
|
181
189
|
export function hasAnyIsland(def) {
|
|
182
190
|
if (!def || typeof def !== "object") return false;
|
|
183
191
|
if (isDynamic(def)) return true;
|
|
184
|
-
if (Array.isArray(def.children))
|
|
185
|
-
return def.children.some(/** @param {any} c */ (c) => hasAnyIsland(c));
|
|
192
|
+
if (Array.isArray(def.children)) return def.children.some((c) => hasAnyIsland(c));
|
|
186
193
|
return false;
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
// ─── Scope / value resolution ─────────────────────────────────────────────────
|
|
190
197
|
|
|
191
198
|
/**
|
|
192
|
-
* @param {
|
|
193
|
-
* @param {
|
|
194
|
-
* @param {Record<string,
|
|
195
|
-
* @param {Record<string,
|
|
196
|
-
* @returns {{
|
|
199
|
+
* @param {JxElement | JxMutableNode | null} raw
|
|
200
|
+
* @param {Record<string, unknown> | null} [parentScope]
|
|
201
|
+
* @param {Record<string, unknown>} [scopeDefs]
|
|
202
|
+
* @param {Record<string, string>} [media]
|
|
203
|
+
* @returns {{
|
|
204
|
+
* scope: Record<string, unknown>;
|
|
205
|
+
* scopeDefs: Record<string, unknown>;
|
|
206
|
+
* media: Record<string, string>;
|
|
207
|
+
* }}
|
|
197
208
|
*/
|
|
198
209
|
export function createCompileContext(raw, parentScope = null, scopeDefs = {}, media = {}) {
|
|
199
210
|
const scope = raw?.state
|
|
@@ -203,9 +214,9 @@ export function createCompileContext(raw, parentScope = null, scopeDefs = {}, me
|
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
/**
|
|
206
|
-
* @param {Record<string,
|
|
207
|
-
* @param {
|
|
208
|
-
* @returns {
|
|
217
|
+
* @param {Record<string, JxStateDefinition>} [defs]
|
|
218
|
+
* @param {Record<string, unknown> | null} [parentScope]
|
|
219
|
+
* @returns {Record<string, unknown>}
|
|
209
220
|
*/
|
|
210
221
|
export function buildInitialScope(defs = {}, parentScope = null) {
|
|
211
222
|
const scope = Object.create(parentScope ?? null);
|
|
@@ -215,12 +226,13 @@ export function buildInitialScope(defs = {}, parentScope = null) {
|
|
|
215
226
|
setOwnScopeValue(scope, key, cloneValue(def));
|
|
216
227
|
continue;
|
|
217
228
|
}
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
const d = /** @type {JxStateObject & JxPrototypeDef} */ (def);
|
|
230
|
+
if ("default" in d) {
|
|
231
|
+
setOwnScopeValue(scope, key, cloneValue(d.default));
|
|
220
232
|
continue;
|
|
221
233
|
}
|
|
222
|
-
if (!
|
|
223
|
-
setOwnScopeValue(scope, key, cloneValue(
|
|
234
|
+
if (!d.$prototype && !isSchemaOnly(d)) {
|
|
235
|
+
setOwnScopeValue(scope, key, cloneValue(d));
|
|
224
236
|
}
|
|
225
237
|
}
|
|
226
238
|
|
|
@@ -230,21 +242,22 @@ export function buildInitialScope(defs = {}, parentScope = null) {
|
|
|
230
242
|
continue;
|
|
231
243
|
}
|
|
232
244
|
if (!def || typeof def !== "object") continue;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
const d = /** @type {JxStateObject & JxPrototypeDef} */ (def);
|
|
246
|
+
if (d.$prototype === "Function") {
|
|
247
|
+
if (d.body) {
|
|
248
|
+
const fn = new Function("state", ...(d.parameters ?? d.arguments ?? []), d.body);
|
|
249
|
+
if (d.body.includes("return")) {
|
|
237
250
|
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
238
251
|
} else {
|
|
239
252
|
setOwnScopeValue(scope, key, fn);
|
|
240
253
|
}
|
|
241
|
-
} else if (!
|
|
254
|
+
} else if (!d.body?.includes("return")) {
|
|
242
255
|
setOwnScopeValue(scope, key, () => {});
|
|
243
256
|
}
|
|
244
257
|
continue;
|
|
245
258
|
}
|
|
246
|
-
if (
|
|
247
|
-
setOwnScopeValue(scope, key, cloneValue(
|
|
259
|
+
if (d.$prototype === "LocalStorage" || d.$prototype === "SessionStorage") {
|
|
260
|
+
setOwnScopeValue(scope, key, cloneValue(d.default ?? null));
|
|
248
261
|
}
|
|
249
262
|
}
|
|
250
263
|
|
|
@@ -252,9 +265,9 @@ export function buildInitialScope(defs = {}, parentScope = null) {
|
|
|
252
265
|
}
|
|
253
266
|
|
|
254
267
|
/**
|
|
255
|
-
* @param {
|
|
268
|
+
* @param {Record<string, unknown>} scope
|
|
256
269
|
* @param {string} key
|
|
257
|
-
* @param {
|
|
270
|
+
* @param {unknown} value
|
|
258
271
|
*/
|
|
259
272
|
export function setOwnScopeValue(scope, key, value) {
|
|
260
273
|
Object.defineProperty(scope, key, {
|
|
@@ -266,9 +279,9 @@ export function setOwnScopeValue(scope, key, value) {
|
|
|
266
279
|
}
|
|
267
280
|
|
|
268
281
|
/**
|
|
269
|
-
* @param {
|
|
282
|
+
* @param {Record<string, unknown>} scope
|
|
270
283
|
* @param {string} key
|
|
271
|
-
* @param {() =>
|
|
284
|
+
* @param {() => unknown} getter
|
|
272
285
|
*/
|
|
273
286
|
export function defineLazyScopeValue(scope, key, getter) {
|
|
274
287
|
Object.defineProperty(scope, key, {
|
|
@@ -279,35 +292,40 @@ export function defineLazyScopeValue(scope, key, getter) {
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
/**
|
|
282
|
-
* @param {
|
|
283
|
-
* @param {
|
|
284
|
-
* @returns {
|
|
295
|
+
* @param {unknown} value
|
|
296
|
+
* @param {Record<string, unknown>} scope
|
|
297
|
+
* @returns {unknown}
|
|
285
298
|
*/
|
|
286
299
|
export function resolveStaticValue(value, scope) {
|
|
287
|
-
if (isRefObject(value)) return resolveRefValue(value.$ref, scope);
|
|
288
|
-
if (isTemplateString(value)) return evaluateStaticTemplate(value, scope);
|
|
300
|
+
if (isRefObject(value)) return resolveRefValue(/** @type {JxMutableNode} */ (value).$ref, scope);
|
|
301
|
+
if (isTemplateString(value)) return evaluateStaticTemplate(/** @type {string} */ (value), scope);
|
|
289
302
|
return value;
|
|
290
303
|
}
|
|
291
304
|
|
|
292
305
|
/**
|
|
293
|
-
* @param {
|
|
306
|
+
* @param {unknown} value
|
|
294
307
|
* @returns {boolean}
|
|
295
308
|
*/
|
|
296
309
|
export function isRefObject(value) {
|
|
297
|
-
return
|
|
310
|
+
return (
|
|
311
|
+
value !== null &&
|
|
312
|
+
typeof value === "object" &&
|
|
313
|
+
typeof (/** @type {JxMutableNode} */ (value).$ref) === "string"
|
|
314
|
+
);
|
|
298
315
|
}
|
|
299
316
|
|
|
300
317
|
/**
|
|
301
|
-
* @param {
|
|
302
|
-
* @param {
|
|
303
|
-
* @returns {
|
|
318
|
+
* @param {unknown} refValue
|
|
319
|
+
* @param {Record<string, unknown>} scope
|
|
320
|
+
* @returns {unknown}
|
|
304
321
|
*/
|
|
305
322
|
export function resolveRefValue(refValue, scope) {
|
|
306
323
|
if (typeof refValue !== "string") return refValue;
|
|
307
324
|
if (refValue.startsWith("$map/")) {
|
|
308
325
|
const parts = refValue.split("/");
|
|
309
326
|
const key = parts[1];
|
|
310
|
-
const base = scope.$map?.[key] ??
|
|
327
|
+
const base = /** @type {Record<string, unknown> | undefined} */ (scope.$map)?.[key] ??
|
|
328
|
+
scope["$map/" + key];
|
|
311
329
|
return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
|
|
312
330
|
}
|
|
313
331
|
if (refValue.startsWith("#/state/")) {
|
|
@@ -321,13 +339,11 @@ export function resolveRefValue(refValue, scope) {
|
|
|
321
339
|
|
|
322
340
|
/**
|
|
323
341
|
* @param {string} str
|
|
324
|
-
* @param {
|
|
325
|
-
* @returns {
|
|
342
|
+
* @param {Record<string, unknown>} scope
|
|
343
|
+
* @returns {unknown}
|
|
326
344
|
*/
|
|
327
345
|
export function evaluateStaticTemplate(str, scope) {
|
|
328
346
|
try {
|
|
329
|
-
// When the entire string is a single ${...} expression, evaluate it directly
|
|
330
|
-
// to preserve the return type (boolean, number, etc.) instead of stringifying.
|
|
331
347
|
const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
|
|
332
348
|
if (singleExprMatch) {
|
|
333
349
|
const fn = new Function("state", "$map", `return (${singleExprMatch[1]})`);
|
|
@@ -341,23 +357,26 @@ export function evaluateStaticTemplate(str, scope) {
|
|
|
341
357
|
}
|
|
342
358
|
|
|
343
359
|
/**
|
|
344
|
-
* @param {
|
|
360
|
+
* @param {unknown} base
|
|
345
361
|
* @param {string} path
|
|
346
|
-
* @returns {
|
|
362
|
+
* @returns {unknown}
|
|
347
363
|
*/
|
|
348
364
|
export function getPathValue(base, path) {
|
|
349
365
|
if (!path) return base;
|
|
350
|
-
return path
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
366
|
+
return path.split("/").reduce(
|
|
367
|
+
/**
|
|
368
|
+
* @param {Record<string, unknown>} acc
|
|
369
|
+
* @param {string} key
|
|
370
|
+
* @returns {Record<string, unknown>}
|
|
371
|
+
*/
|
|
372
|
+
(acc, key) => (/** @type {Record<string, unknown>} */ (acc == null ? undefined : acc[key])),
|
|
373
|
+
/** @type {Record<string, unknown>} */ (base),
|
|
374
|
+
);
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
/**
|
|
359
|
-
* @param {
|
|
360
|
-
* @returns {
|
|
378
|
+
* @param {unknown} value
|
|
379
|
+
* @returns {unknown}
|
|
361
380
|
*/
|
|
362
381
|
export function cloneValue(value) {
|
|
363
382
|
if (value === null || typeof value !== "object") return value;
|
|
@@ -369,9 +388,8 @@ export function cloneValue(value) {
|
|
|
369
388
|
/**
|
|
370
389
|
* Build an HTML attribute string from a static element definition.
|
|
371
390
|
*
|
|
372
|
-
* @param {
|
|
373
|
-
* @param {
|
|
374
|
-
* @returns {string}
|
|
391
|
+
* @param {JxElement | JxMutableNode} def
|
|
392
|
+
* @param {Record<string, unknown>} scope @returns {string}
|
|
375
393
|
*/
|
|
376
394
|
export function buildAttrs(def, scope) {
|
|
377
395
|
let out = "";
|
|
@@ -384,14 +402,14 @@ export function buildAttrs(def, scope) {
|
|
|
384
402
|
const lang = resolveStaticValue(def.lang, scope);
|
|
385
403
|
const dir = resolveStaticValue(def.dir, scope);
|
|
386
404
|
|
|
387
|
-
if (id) out += ` id="${escapeHtml(id)}"`;
|
|
388
|
-
if (className) out += ` class="${escapeHtml(className)}"`;
|
|
405
|
+
if (id) out += ` id="${escapeHtml(String(id))}"`;
|
|
406
|
+
if (className) out += ` class="${escapeHtml(String(className))}"`;
|
|
389
407
|
if (hidden) out += " hidden";
|
|
390
408
|
if (tabIndex !== undefined && tabIndex !== null)
|
|
391
409
|
out += ` tabindex="${escapeHtml(String(tabIndex))}"`;
|
|
392
|
-
if (title) out += ` title="${escapeHtml(title)}"`;
|
|
393
|
-
if (lang) out += ` lang="${escapeHtml(lang)}"`;
|
|
394
|
-
if (dir) out += ` dir="${escapeHtml(dir)}"`;
|
|
410
|
+
if (title) out += ` title="${escapeHtml(String(title))}"`;
|
|
411
|
+
if (lang) out += ` lang="${escapeHtml(String(lang))}"`;
|
|
412
|
+
if (dir) out += ` dir="${escapeHtml(String(dir))}"`;
|
|
395
413
|
|
|
396
414
|
if (def.style && scope) {
|
|
397
415
|
const inline = Object.entries(def.style)
|
|
@@ -445,9 +463,13 @@ export function buildAttrs(def, scope) {
|
|
|
445
463
|
/**
|
|
446
464
|
* Build the inner HTML (textContent or children) for a node.
|
|
447
465
|
*
|
|
448
|
-
* @param {
|
|
449
|
-
* @param {
|
|
450
|
-
* @param {{
|
|
466
|
+
* @param {JxElement} def
|
|
467
|
+
* @param {JxElement | null} raw
|
|
468
|
+
* @param {{
|
|
469
|
+
* scope: Record<string, unknown>;
|
|
470
|
+
* scopeDefs: Record<string, unknown>;
|
|
471
|
+
* media: Record<string, string>;
|
|
472
|
+
* }} context
|
|
451
473
|
* @param {(def: any, raw: any, context: any) => string} childCompiler
|
|
452
474
|
* @returns {string}
|
|
453
475
|
*/
|
|
@@ -458,12 +480,13 @@ export function buildInner(def, raw, context, childCompiler) {
|
|
|
458
480
|
const value = resolveStaticValue(source.textContent, context.scope);
|
|
459
481
|
return value == null ? "" : escapeHtml(String(value));
|
|
460
482
|
}
|
|
461
|
-
if (source.innerHTML)
|
|
483
|
+
if (source.innerHTML)
|
|
484
|
+
return /** @type {string} */ (resolveStaticValue(source.innerHTML, context.scope)) ?? "";
|
|
462
485
|
if (Array.isArray(source.children)) {
|
|
463
486
|
const rawChildren = raw?.children;
|
|
464
487
|
return source.children
|
|
465
|
-
.map((/** @type {
|
|
466
|
-
const childRaw = rawChildren?.[i] ?? c;
|
|
488
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c, /** @type {number} */ i) => {
|
|
489
|
+
const childRaw = /** @type {(JxElement | string)[] | undefined} */ (rawChildren)?.[i] ?? c;
|
|
467
490
|
return childCompiler(c, childRaw, context);
|
|
468
491
|
})
|
|
469
492
|
.join("\n ");
|
|
@@ -476,8 +499,9 @@ export function buildInner(def, raw, context, childCompiler) {
|
|
|
476
499
|
/**
|
|
477
500
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
478
501
|
*
|
|
479
|
-
* @param {
|
|
480
|
-
* @param {Record<string,
|
|
502
|
+
* @param {JxElement | JxMutableNode} doc
|
|
503
|
+
* @param {Record<string, string>} [mediaQueries]
|
|
504
|
+
* @param {JxStyle | null} [projectStyle]
|
|
481
505
|
* @returns {string}
|
|
482
506
|
*/
|
|
483
507
|
export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
@@ -488,11 +512,8 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
488
512
|
// everything else on body. Project-level style is implicitly :root, so a
|
|
489
513
|
// flat object like { "--bg": "#000", "margin": "0" } is the expected format.
|
|
490
514
|
if (projectStyle && typeof projectStyle === "object") {
|
|
491
|
-
function emitProjectRules(
|
|
492
|
-
|
|
493
|
-
/** @type {Record<string, any>} */ obj,
|
|
494
|
-
) {
|
|
495
|
-
const props = toCSSText(/** @type {any} */ (obj));
|
|
515
|
+
function emitProjectRules(/** @type {string} */ selector, /** @type {JxMutableNode} */ obj) {
|
|
516
|
+
const props = toCSSText(obj);
|
|
496
517
|
if (props) rules.push(`${selector} { ${props} }`);
|
|
497
518
|
for (const [key, val] of Object.entries(obj)) {
|
|
498
519
|
if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
|
|
@@ -500,9 +521,9 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
500
521
|
const query = key.startsWith("@--")
|
|
501
522
|
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
502
523
|
: key.slice(1);
|
|
503
|
-
const mProps = toCSSText(
|
|
524
|
+
const mProps = toCSSText(val);
|
|
504
525
|
if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
|
|
505
|
-
for (const [sel, sub] of Object.entries(/** @type {Record<string,
|
|
526
|
+
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
|
|
506
527
|
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
|
|
507
528
|
if (sel.startsWith("@")) continue;
|
|
508
529
|
const resolved = sel.startsWith("&")
|
|
@@ -510,7 +531,7 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
510
531
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
511
532
|
? `${selector}${sel}`
|
|
512
533
|
: `${selector} ${sel}`;
|
|
513
|
-
const subProps = toCSSText(
|
|
534
|
+
const subProps = toCSSText(sub);
|
|
514
535
|
if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
|
|
515
536
|
}
|
|
516
537
|
continue;
|
|
@@ -520,13 +541,13 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
520
541
|
: key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
|
|
521
542
|
? `${selector}${key}`
|
|
522
543
|
: `${selector} ${key}`;
|
|
523
|
-
emitProjectRules(resolved, val);
|
|
544
|
+
emitProjectRules(resolved, /** @type {Record<string, unknown>} */ (val));
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
547
|
|
|
527
548
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
528
549
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
529
|
-
emitProjectRules(key, /** @type {
|
|
550
|
+
emitProjectRules(key, /** @type {Record<string, unknown>} */ (val));
|
|
530
551
|
} else if (
|
|
531
552
|
val !== null &&
|
|
532
553
|
typeof val === "object" &&
|
|
@@ -540,14 +561,14 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
540
561
|
const query = key.startsWith("@--")
|
|
541
562
|
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
542
563
|
: key.slice(1);
|
|
543
|
-
rules.push(`@media ${query} { body { ${toCSSText(/** @type {
|
|
564
|
+
rules.push(`@media ${query} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
|
|
544
565
|
}
|
|
545
566
|
}
|
|
546
567
|
// Collect CSS custom properties into :root {}
|
|
547
|
-
/** @type {Record<string,
|
|
568
|
+
/** @type {Record<string, unknown>} */
|
|
548
569
|
const rootProps = {};
|
|
549
570
|
// Collect direct CSS properties into body {}
|
|
550
|
-
/** @type {Record<string,
|
|
571
|
+
/** @type {Record<string, unknown>} */
|
|
551
572
|
const bodyProps = {};
|
|
552
573
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
553
574
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[") || key.startsWith("@"))
|
|
@@ -559,11 +580,11 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
559
580
|
bodyProps[key] = val;
|
|
560
581
|
}
|
|
561
582
|
}
|
|
562
|
-
const rootCSS = toCSSText(
|
|
583
|
+
const rootCSS = toCSSText(rootProps);
|
|
563
584
|
if (rootCSS) {
|
|
564
585
|
rules.push(`:root { ${rootCSS} }`);
|
|
565
586
|
}
|
|
566
|
-
const bodyCSS = toCSSText(
|
|
587
|
+
const bodyCSS = toCSSText(bodyProps);
|
|
567
588
|
if (bodyCSS) {
|
|
568
589
|
rules.push(`body { ${bodyCSS} }`);
|
|
569
590
|
}
|
|
@@ -579,12 +600,12 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
579
600
|
* Recursively emit CSS rules for a nested element selector.
|
|
580
601
|
*
|
|
581
602
|
* @param {string} selector
|
|
582
|
-
* @param {Record<string,
|
|
603
|
+
* @param {Record<string, unknown>} obj
|
|
583
604
|
* @param {string[]} rules
|
|
584
|
-
* @param {Record<string,
|
|
605
|
+
* @param {Record<string, string>} mediaQueries
|
|
585
606
|
*/
|
|
586
607
|
function emitNestedElement(selector, obj, rules, mediaQueries) {
|
|
587
|
-
const props = toCSSText(
|
|
608
|
+
const props = toCSSText(obj);
|
|
588
609
|
if (props) rules.push(`${selector} { ${props} }`);
|
|
589
610
|
for (const [key, val] of Object.entries(obj)) {
|
|
590
611
|
if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
|
|
@@ -592,9 +613,9 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
|
|
|
592
613
|
const query = key.startsWith("@--")
|
|
593
614
|
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
594
615
|
: key.slice(1);
|
|
595
|
-
const mProps = toCSSText(
|
|
616
|
+
const mProps = toCSSText(val);
|
|
596
617
|
if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
|
|
597
|
-
for (const [sel, sub] of Object.entries(/** @type {Record<string,
|
|
618
|
+
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
|
|
598
619
|
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
|
|
599
620
|
if (sel.startsWith("@")) continue;
|
|
600
621
|
const resolved = sel.startsWith("&")
|
|
@@ -602,7 +623,7 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
|
|
|
602
623
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
603
624
|
? `${selector}${sel}`
|
|
604
625
|
: `${selector} ${sel}`;
|
|
605
|
-
const subProps = toCSSText(
|
|
626
|
+
const subProps = toCSSText(sub);
|
|
606
627
|
if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
|
|
607
628
|
}
|
|
608
629
|
continue;
|
|
@@ -612,14 +633,14 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
|
|
|
612
633
|
: key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
|
|
613
634
|
? `${selector}${key}`
|
|
614
635
|
: `${selector} ${key}`;
|
|
615
|
-
emitNestedElement(resolved, val, rules, mediaQueries);
|
|
636
|
+
emitNestedElement(resolved, /** @type {Record<string, unknown>} */ (val), rules, mediaQueries);
|
|
616
637
|
}
|
|
617
638
|
}
|
|
618
639
|
|
|
619
640
|
/**
|
|
620
|
-
* @param {
|
|
641
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
621
642
|
* @param {string[]} rules
|
|
622
|
-
* @param {Record<string,
|
|
643
|
+
* @param {Record<string, string>} mediaQueries
|
|
623
644
|
* @param {string} [_parentSel]
|
|
624
645
|
* @param {{ n: number }} [counter]
|
|
625
646
|
*/
|
|
@@ -670,8 +691,10 @@ export function collectStyles(
|
|
|
670
691
|
const query = prop.startsWith("@--")
|
|
671
692
|
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
672
693
|
: prop.slice(1);
|
|
673
|
-
rules.push(`@media ${query} { ${selector} { ${toCSSText(
|
|
674
|
-
for (const [sel, nestedRules] of Object.entries(
|
|
694
|
+
rules.push(`@media ${query} { ${selector} { ${toCSSText(val)} } }`);
|
|
695
|
+
for (const [sel, nestedRules] of Object.entries(
|
|
696
|
+
/** @type {Record<string, unknown>} */ (val),
|
|
697
|
+
)) {
|
|
675
698
|
if (nestedRules === null || typeof nestedRules !== "object" || Array.isArray(nestedRules))
|
|
676
699
|
continue;
|
|
677
700
|
if (sel.startsWith("@")) continue;
|
|
@@ -680,9 +703,7 @@ export function collectStyles(
|
|
|
680
703
|
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
681
704
|
? `${selector}${sel}`
|
|
682
705
|
: `${selector} ${sel}`;
|
|
683
|
-
rules.push(
|
|
684
|
-
`@media ${query} { ${resolved} { ${toCSSText(/** @type {any} */ (nestedRules))} } }`,
|
|
685
|
-
);
|
|
706
|
+
rules.push(`@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }`);
|
|
686
707
|
}
|
|
687
708
|
} else {
|
|
688
709
|
const resolved = prop.startsWith("&")
|
|
@@ -690,13 +711,18 @@ export function collectStyles(
|
|
|
690
711
|
: prop.startsWith(":") || prop.startsWith(".") || prop.startsWith("[")
|
|
691
712
|
? `${selector}${prop}`
|
|
692
713
|
: `${selector} ${prop}`;
|
|
693
|
-
emitNestedElement(
|
|
714
|
+
emitNestedElement(
|
|
715
|
+
resolved,
|
|
716
|
+
/** @type {Record<string, unknown>} */ (val),
|
|
717
|
+
rules,
|
|
718
|
+
mediaQueries,
|
|
719
|
+
);
|
|
694
720
|
}
|
|
695
721
|
}
|
|
696
722
|
}
|
|
697
723
|
|
|
698
724
|
if (Array.isArray(def.children)) {
|
|
699
|
-
def.children.forEach((/** @type {
|
|
725
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) => {
|
|
700
726
|
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
701
727
|
});
|
|
702
728
|
}
|
|
@@ -747,7 +773,7 @@ export function tagNameToClassName(tagName) {
|
|
|
747
773
|
/**
|
|
748
774
|
* Recursively collect unique $src values from $prototype: "Function" entries.
|
|
749
775
|
*
|
|
750
|
-
* @param {
|
|
776
|
+
* @param {JxElement} doc
|
|
751
777
|
* @returns {string[]}
|
|
752
778
|
*/
|
|
753
779
|
export function collectSrcImports(doc) {
|
|
@@ -758,7 +784,7 @@ export function collectSrcImports(doc) {
|
|
|
758
784
|
}
|
|
759
785
|
|
|
760
786
|
/**
|
|
761
|
-
* @param {
|
|
787
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
762
788
|
* @param {Set<string>} srcs
|
|
763
789
|
*/
|
|
764
790
|
function _walkSrc(def, srcs) {
|
|
@@ -768,40 +794,42 @@ function _walkSrc(def, srcs) {
|
|
|
768
794
|
if (
|
|
769
795
|
d &&
|
|
770
796
|
typeof d === "object" &&
|
|
771
|
-
/** @type {
|
|
772
|
-
/** @type {
|
|
797
|
+
/** @type {JxMutableNode} */ (d).$prototype === "Function" &&
|
|
798
|
+
/** @type {JxMutableNode} */ (d).$src
|
|
773
799
|
) {
|
|
774
|
-
srcs.add(/** @type {
|
|
800
|
+
srcs.add(/** @type {string} */ (/** @type {JxMutableNode} */ (d).$src));
|
|
775
801
|
}
|
|
776
802
|
}
|
|
777
803
|
}
|
|
778
804
|
if (Array.isArray(def.children)) {
|
|
779
|
-
def.children.forEach((/** @type {
|
|
805
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
806
|
+
_walkSrc(c, srcs),
|
|
807
|
+
);
|
|
780
808
|
}
|
|
781
809
|
}
|
|
782
810
|
|
|
783
811
|
/**
|
|
784
812
|
* Recursively collect all `timing: "server"` entries from the document tree.
|
|
785
813
|
*
|
|
786
|
-
* @param {
|
|
787
|
-
* @returns {
|
|
814
|
+
* @param {JxElement} doc
|
|
815
|
+
* @returns {{ key: string; exportName: string; src: string }[]}
|
|
788
816
|
*/
|
|
789
817
|
export function collectServerEntries(doc) {
|
|
790
|
-
/** @type {Map<string,
|
|
818
|
+
/** @type {Map<string, { key: string; exportName: string; src: string }>} */
|
|
791
819
|
const entries = new Map();
|
|
792
820
|
_walkServerEntries(doc, entries);
|
|
793
821
|
return [...entries.values()];
|
|
794
822
|
}
|
|
795
823
|
|
|
796
824
|
/**
|
|
797
|
-
* @param {
|
|
798
|
-
* @param {Map<string,
|
|
825
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
826
|
+
* @param {Map<string, { key: string; exportName: string; src: string }>} entries
|
|
799
827
|
*/
|
|
800
828
|
function _walkServerEntries(def, entries) {
|
|
801
829
|
if (!def || typeof def !== "object") return;
|
|
802
830
|
if (def.state) {
|
|
803
831
|
for (const [key, d] of Object.entries(def.state)) {
|
|
804
|
-
const entry = /** @type {
|
|
832
|
+
const entry = /** @type {JxMutableNode} */ (d);
|
|
805
833
|
if (
|
|
806
834
|
entry &&
|
|
807
835
|
typeof entry === "object" &&
|
|
@@ -815,7 +843,9 @@ function _walkServerEntries(def, entries) {
|
|
|
815
843
|
}
|
|
816
844
|
}
|
|
817
845
|
if (Array.isArray(def.children)) {
|
|
818
|
-
def.children.forEach((/** @type {
|
|
846
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
847
|
+
_walkServerEntries(c, entries),
|
|
848
|
+
);
|
|
819
849
|
}
|
|
820
850
|
}
|
|
821
851
|
|
|
@@ -827,8 +857,8 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
|
|
|
827
857
|
/**
|
|
828
858
|
* Recursively render a Jx node tree to static HTML for pre-rendering.
|
|
829
859
|
*
|
|
830
|
-
* @param {
|
|
831
|
-
* @param {
|
|
860
|
+
* @param {JxElement | JxMutableNode | string} node
|
|
861
|
+
* @param {Record<string, unknown>} scope
|
|
832
862
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
833
863
|
* @returns {string}
|
|
834
864
|
*/
|
|
@@ -842,7 +872,11 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
842
872
|
}
|
|
843
873
|
if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
|
|
844
874
|
if (Array.isArray(node))
|
|
845
|
-
return node
|
|
875
|
+
return node
|
|
876
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
877
|
+
renderStaticNode(c, scope, slotContent),
|
|
878
|
+
)
|
|
879
|
+
.join("\n");
|
|
846
880
|
if (!node || typeof node !== "object") return "";
|
|
847
881
|
|
|
848
882
|
// Skip mapped arrays — can't pre-render dynamic lists
|
|
@@ -863,10 +897,12 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
863
897
|
inner = val != null ? escapeHtml(String(val)) : "";
|
|
864
898
|
} else if (node.innerHTML) {
|
|
865
899
|
const val = resolveStaticValue(node.innerHTML, scope);
|
|
866
|
-
inner = val != null ? val : node.innerHTML;
|
|
900
|
+
inner = val != null ? String(val) : /** @type {string} */ (node.innerHTML);
|
|
867
901
|
} else if (Array.isArray(node.children)) {
|
|
868
902
|
inner = node.children
|
|
869
|
-
.map((/** @type {
|
|
903
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
904
|
+
renderStaticNode(c, scope, slotContent),
|
|
905
|
+
)
|
|
870
906
|
.join("\n");
|
|
871
907
|
}
|
|
872
908
|
|
|
@@ -876,39 +912,41 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
876
912
|
/**
|
|
877
913
|
* Pre-render a component definition to static HTML for its inner content.
|
|
878
914
|
*
|
|
879
|
-
* @param {
|
|
880
|
-
* @param {Record<string,
|
|
881
|
-
* state
|
|
915
|
+
* @param {JxElement} doc - Component JSON definition
|
|
916
|
+
* @param {Record<string, unknown> | null} [propsOverride] - Instance-specific prop values to merge
|
|
917
|
+
* into state
|
|
882
918
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
883
919
|
* @returns {string} The pre-rendered innerHTML
|
|
884
920
|
*/
|
|
885
921
|
export function preRenderComponentHtml(doc, propsOverride = null, slotContent = null) {
|
|
922
|
+
/** @type {Record<string, JxStateDefinition>} */
|
|
886
923
|
let stateDefs = doc.state ?? {};
|
|
887
924
|
if (propsOverride) {
|
|
888
925
|
stateDefs = { ...stateDefs };
|
|
889
926
|
for (const [key, value] of Object.entries(propsOverride)) {
|
|
890
927
|
if (key in stateDefs) {
|
|
891
928
|
const existing = stateDefs[key];
|
|
892
|
-
// If the existing definition is an expanded signal with "default", override the default
|
|
893
929
|
if (
|
|
894
930
|
existing &&
|
|
895
931
|
typeof existing === "object" &&
|
|
896
932
|
!Array.isArray(existing) &&
|
|
897
933
|
"default" in existing
|
|
898
934
|
) {
|
|
899
|
-
stateDefs[key] = {
|
|
935
|
+
stateDefs[key] = { .../** @type {JxStateObject} */ (existing), default: value };
|
|
900
936
|
} else {
|
|
901
|
-
stateDefs[key] = value;
|
|
937
|
+
stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
902
938
|
}
|
|
903
939
|
} else {
|
|
904
|
-
stateDefs[key] = value;
|
|
940
|
+
stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
905
941
|
}
|
|
906
942
|
}
|
|
907
943
|
}
|
|
908
944
|
const scope = buildInitialScope(stateDefs, null);
|
|
909
945
|
if (!Array.isArray(doc.children)) return "";
|
|
910
946
|
return doc.children
|
|
911
|
-
.map((/** @type {
|
|
947
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
948
|
+
renderStaticNode(c, scope, slotContent),
|
|
949
|
+
)
|
|
912
950
|
.join("\n");
|
|
913
951
|
}
|
|
914
952
|
|
|
@@ -918,7 +956,7 @@ export function preRenderComponentHtml(doc, propsOverride = null, slotContent =
|
|
|
918
956
|
* Returns true when: no event handlers, no $prototype entries (Functions, Request, Storage), no
|
|
919
957
|
* $ref values. Conservative — returns false when uncertain.
|
|
920
958
|
*
|
|
921
|
-
* @param {
|
|
959
|
+
* @param {JxElement} doc - Component JSON definition
|
|
922
960
|
* @returns {boolean}
|
|
923
961
|
*/
|
|
924
962
|
export function isComponentFullyStatic(doc) {
|
|
@@ -926,7 +964,7 @@ export function isComponentFullyStatic(doc) {
|
|
|
926
964
|
}
|
|
927
965
|
|
|
928
966
|
/**
|
|
929
|
-
* @param {
|
|
967
|
+
* @param {JxElement | string | (JxElement | string)[]} node
|
|
930
968
|
* @returns {boolean}
|
|
931
969
|
*/
|
|
932
970
|
function _isStaticNode(node) {
|
|
@@ -942,7 +980,7 @@ function _isStaticNode(node) {
|
|
|
942
980
|
if (node.state) {
|
|
943
981
|
for (const def of Object.values(node.state)) {
|
|
944
982
|
if (!def || typeof def !== "object") continue;
|
|
945
|
-
const d = /** @type {
|
|
983
|
+
const d = /** @type {JxMutableNode} */ (def);
|
|
946
984
|
if (d.$prototype) return false;
|
|
947
985
|
if (d.$ref) return false;
|
|
948
986
|
}
|
|
@@ -969,9 +1007,9 @@ function _isStaticNode(node) {
|
|
|
969
1007
|
* styles using .jx-N selectors via collectStyles.
|
|
970
1008
|
*
|
|
971
1009
|
* @param {string} tagName - The custom element tag name (used as CSS selector)
|
|
972
|
-
* @param {
|
|
973
|
-
* @param {
|
|
974
|
-
* @param {Record<string,
|
|
1010
|
+
* @param {JxStyle | null} styleDef - The component's style object
|
|
1011
|
+
* @param {JxElement | null} [doc] - The full component document (for walking children)
|
|
1012
|
+
* @param {Record<string, string>} [mediaQueries] - Project media query definitions
|
|
975
1013
|
* @returns {string} CSS text, or empty string if no styles
|
|
976
1014
|
*/
|
|
977
1015
|
export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
|
|
@@ -1003,7 +1041,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
|
|
|
1003
1041
|
const query = prop.startsWith("@--")
|
|
1004
1042
|
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
1005
1043
|
: prop.slice(1);
|
|
1006
|
-
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {
|
|
1044
|
+
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
|
|
1007
1045
|
} else if (
|
|
1008
1046
|
prop.startsWith(":") ||
|
|
1009
1047
|
prop.startsWith(".") ||
|
|
@@ -1011,7 +1049,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
|
|
|
1011
1049
|
prop.startsWith("[")
|
|
1012
1050
|
) {
|
|
1013
1051
|
const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
|
|
1014
|
-
rules.push(`${resolved} { ${toCSSText(/** @type {
|
|
1052
|
+
rules.push(`${resolved} { ${toCSSText(/** @type {object} */ (val))} }`);
|
|
1015
1053
|
}
|
|
1016
1054
|
}
|
|
1017
1055
|
}
|