@jxsuite/compiler 0.18.0 → 0.20.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 +111 -111
- package/package.json +6 -4
- package/src/cli.js +1 -1
- package/src/compiler.js +2 -2
- package/src/shared.js +260 -149
- package/src/site/content-loader.js +84 -71
- 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,10 +512,42 @@ 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") {
|
|
515
|
+
function emitProjectRules(/** @type {string} */ selector, /** @type {JxMutableNode} */ obj) {
|
|
516
|
+
const props = toCSSText(obj);
|
|
517
|
+
if (props) rules.push(`${selector} { ${props} }`);
|
|
518
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
519
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
|
|
520
|
+
if (key.startsWith("@")) {
|
|
521
|
+
const query = key.startsWith("@--")
|
|
522
|
+
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
523
|
+
: key.slice(1);
|
|
524
|
+
const mProps = toCSSText(val);
|
|
525
|
+
if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
|
|
526
|
+
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
|
|
527
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
|
|
528
|
+
if (sel.startsWith("@")) continue;
|
|
529
|
+
const resolved = sel.startsWith("&")
|
|
530
|
+
? sel.replace("&", selector)
|
|
531
|
+
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
532
|
+
? `${selector}${sel}`
|
|
533
|
+
: `${selector} ${sel}`;
|
|
534
|
+
const subProps = toCSSText(sub);
|
|
535
|
+
if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const resolved = key.startsWith("&")
|
|
540
|
+
? key.replace("&", selector)
|
|
541
|
+
: key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
|
|
542
|
+
? `${selector}${key}`
|
|
543
|
+
: `${selector} ${key}`;
|
|
544
|
+
emitProjectRules(resolved, /** @type {Record<string, unknown>} */ (val));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
491
548
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
492
549
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
493
|
-
|
|
494
|
-
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
550
|
+
emitProjectRules(key, /** @type {Record<string, unknown>} */ (val));
|
|
495
551
|
} else if (
|
|
496
552
|
val !== null &&
|
|
497
553
|
typeof val === "object" &&
|
|
@@ -499,21 +555,20 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
499
555
|
!key.startsWith("@") &&
|
|
500
556
|
!key.startsWith("--")
|
|
501
557
|
) {
|
|
502
|
-
|
|
503
|
-
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
558
|
+
emitProjectRules(key, val);
|
|
504
559
|
} else if (key.startsWith("@")) {
|
|
505
|
-
// @media block
|
|
560
|
+
// @media block at top level
|
|
506
561
|
const query = key.startsWith("@--")
|
|
507
562
|
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
508
563
|
: key.slice(1);
|
|
509
|
-
rules.push(`@media ${query} { body { ${toCSSText(/** @type {
|
|
564
|
+
rules.push(`@media ${query} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
|
|
510
565
|
}
|
|
511
566
|
}
|
|
512
567
|
// Collect CSS custom properties into :root {}
|
|
513
|
-
/** @type {Record<string,
|
|
568
|
+
/** @type {Record<string, unknown>} */
|
|
514
569
|
const rootProps = {};
|
|
515
570
|
// Collect direct CSS properties into body {}
|
|
516
|
-
/** @type {Record<string,
|
|
571
|
+
/** @type {Record<string, unknown>} */
|
|
517
572
|
const bodyProps = {};
|
|
518
573
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
519
574
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[") || key.startsWith("@"))
|
|
@@ -525,11 +580,11 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
525
580
|
bodyProps[key] = val;
|
|
526
581
|
}
|
|
527
582
|
}
|
|
528
|
-
const rootCSS = toCSSText(
|
|
583
|
+
const rootCSS = toCSSText(rootProps);
|
|
529
584
|
if (rootCSS) {
|
|
530
585
|
rules.push(`:root { ${rootCSS} }`);
|
|
531
586
|
}
|
|
532
|
-
const bodyCSS = toCSSText(
|
|
587
|
+
const bodyCSS = toCSSText(bodyProps);
|
|
533
588
|
if (bodyCSS) {
|
|
534
589
|
rules.push(`body { ${bodyCSS} }`);
|
|
535
590
|
}
|
|
@@ -542,9 +597,50 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
542
597
|
}
|
|
543
598
|
|
|
544
599
|
/**
|
|
545
|
-
*
|
|
600
|
+
* Recursively emit CSS rules for a nested element selector.
|
|
601
|
+
*
|
|
602
|
+
* @param {string} selector
|
|
603
|
+
* @param {Record<string, unknown>} obj
|
|
604
|
+
* @param {string[]} rules
|
|
605
|
+
* @param {Record<string, string>} mediaQueries
|
|
606
|
+
*/
|
|
607
|
+
function emitNestedElement(selector, obj, rules, mediaQueries) {
|
|
608
|
+
const props = toCSSText(obj);
|
|
609
|
+
if (props) rules.push(`${selector} { ${props} }`);
|
|
610
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
611
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
|
|
612
|
+
if (key.startsWith("@")) {
|
|
613
|
+
const query = key.startsWith("@--")
|
|
614
|
+
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
615
|
+
: key.slice(1);
|
|
616
|
+
const mProps = toCSSText(val);
|
|
617
|
+
if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
|
|
618
|
+
for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
|
|
619
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
|
|
620
|
+
if (sel.startsWith("@")) continue;
|
|
621
|
+
const resolved = sel.startsWith("&")
|
|
622
|
+
? sel.replace("&", selector)
|
|
623
|
+
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
624
|
+
? `${selector}${sel}`
|
|
625
|
+
: `${selector} ${sel}`;
|
|
626
|
+
const subProps = toCSSText(sub);
|
|
627
|
+
if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
|
|
628
|
+
}
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const resolved = key.startsWith("&")
|
|
632
|
+
? key.replace("&", selector)
|
|
633
|
+
: key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
|
|
634
|
+
? `${selector}${key}`
|
|
635
|
+
: `${selector} ${key}`;
|
|
636
|
+
emitNestedElement(resolved, /** @type {Record<string, unknown>} */ (val), rules, mediaQueries);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
546
642
|
* @param {string[]} rules
|
|
547
|
-
* @param {Record<string,
|
|
643
|
+
* @param {Record<string, string>} mediaQueries
|
|
548
644
|
* @param {string} [_parentSel]
|
|
549
645
|
* @param {{ n: number }} [counter]
|
|
550
646
|
*/
|
|
@@ -590,40 +686,43 @@ export function collectStyles(
|
|
|
590
686
|
}
|
|
591
687
|
|
|
592
688
|
for (const [prop, val] of Object.entries(def.style)) {
|
|
689
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
|
|
593
690
|
if (prop.startsWith("@")) {
|
|
594
691
|
const query = prop.startsWith("@--")
|
|
595
692
|
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
596
693
|
: prop.slice(1);
|
|
597
|
-
rules.push(`@media ${query} { ${selector} { ${toCSSText(
|
|
598
|
-
for (const [sel, nestedRules] of Object.entries(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
sel.startsWith(".") ||
|
|
603
|
-
sel.startsWith("&") ||
|
|
604
|
-
sel.startsWith("[")
|
|
605
|
-
)
|
|
606
|
-
)
|
|
694
|
+
rules.push(`@media ${query} { ${selector} { ${toCSSText(val)} } }`);
|
|
695
|
+
for (const [sel, nestedRules] of Object.entries(
|
|
696
|
+
/** @type {Record<string, unknown>} */ (val),
|
|
697
|
+
)) {
|
|
698
|
+
if (nestedRules === null || typeof nestedRules !== "object" || Array.isArray(nestedRules))
|
|
607
699
|
continue;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
700
|
+
if (sel.startsWith("@")) continue;
|
|
701
|
+
const resolved = sel.startsWith("&")
|
|
702
|
+
? sel.replace("&", selector)
|
|
703
|
+
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
704
|
+
? `${selector}${sel}`
|
|
705
|
+
: `${selector} ${sel}`;
|
|
706
|
+
rules.push(`@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }`);
|
|
612
707
|
}
|
|
613
|
-
} else
|
|
614
|
-
prop.startsWith("
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
708
|
+
} else {
|
|
709
|
+
const resolved = prop.startsWith("&")
|
|
710
|
+
? prop.replace("&", selector)
|
|
711
|
+
: prop.startsWith(":") || prop.startsWith(".") || prop.startsWith("[")
|
|
712
|
+
? `${selector}${prop}`
|
|
713
|
+
: `${selector} ${prop}`;
|
|
714
|
+
emitNestedElement(
|
|
715
|
+
resolved,
|
|
716
|
+
/** @type {Record<string, unknown>} */ (val),
|
|
717
|
+
rules,
|
|
718
|
+
mediaQueries,
|
|
719
|
+
);
|
|
621
720
|
}
|
|
622
721
|
}
|
|
623
722
|
}
|
|
624
723
|
|
|
625
724
|
if (Array.isArray(def.children)) {
|
|
626
|
-
def.children.forEach((/** @type {
|
|
725
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) => {
|
|
627
726
|
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
628
727
|
});
|
|
629
728
|
}
|
|
@@ -674,7 +773,7 @@ export function tagNameToClassName(tagName) {
|
|
|
674
773
|
/**
|
|
675
774
|
* Recursively collect unique $src values from $prototype: "Function" entries.
|
|
676
775
|
*
|
|
677
|
-
* @param {
|
|
776
|
+
* @param {JxElement} doc
|
|
678
777
|
* @returns {string[]}
|
|
679
778
|
*/
|
|
680
779
|
export function collectSrcImports(doc) {
|
|
@@ -685,7 +784,7 @@ export function collectSrcImports(doc) {
|
|
|
685
784
|
}
|
|
686
785
|
|
|
687
786
|
/**
|
|
688
|
-
* @param {
|
|
787
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
689
788
|
* @param {Set<string>} srcs
|
|
690
789
|
*/
|
|
691
790
|
function _walkSrc(def, srcs) {
|
|
@@ -695,40 +794,42 @@ function _walkSrc(def, srcs) {
|
|
|
695
794
|
if (
|
|
696
795
|
d &&
|
|
697
796
|
typeof d === "object" &&
|
|
698
|
-
/** @type {
|
|
699
|
-
/** @type {
|
|
797
|
+
/** @type {JxMutableNode} */ (d).$prototype === "Function" &&
|
|
798
|
+
/** @type {JxMutableNode} */ (d).$src
|
|
700
799
|
) {
|
|
701
|
-
srcs.add(/** @type {
|
|
800
|
+
srcs.add(/** @type {string} */ (/** @type {JxMutableNode} */ (d).$src));
|
|
702
801
|
}
|
|
703
802
|
}
|
|
704
803
|
}
|
|
705
804
|
if (Array.isArray(def.children)) {
|
|
706
|
-
def.children.forEach((/** @type {
|
|
805
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
806
|
+
_walkSrc(c, srcs),
|
|
807
|
+
);
|
|
707
808
|
}
|
|
708
809
|
}
|
|
709
810
|
|
|
710
811
|
/**
|
|
711
812
|
* Recursively collect all `timing: "server"` entries from the document tree.
|
|
712
813
|
*
|
|
713
|
-
* @param {
|
|
714
|
-
* @returns {
|
|
814
|
+
* @param {JxElement} doc
|
|
815
|
+
* @returns {{ key: string; exportName: string; src: string }[]}
|
|
715
816
|
*/
|
|
716
817
|
export function collectServerEntries(doc) {
|
|
717
|
-
/** @type {Map<string,
|
|
818
|
+
/** @type {Map<string, { key: string; exportName: string; src: string }>} */
|
|
718
819
|
const entries = new Map();
|
|
719
820
|
_walkServerEntries(doc, entries);
|
|
720
821
|
return [...entries.values()];
|
|
721
822
|
}
|
|
722
823
|
|
|
723
824
|
/**
|
|
724
|
-
* @param {
|
|
725
|
-
* @param {Map<string,
|
|
825
|
+
* @param {JxElement | JxMutableNode | string} def
|
|
826
|
+
* @param {Map<string, { key: string; exportName: string; src: string }>} entries
|
|
726
827
|
*/
|
|
727
828
|
function _walkServerEntries(def, entries) {
|
|
728
829
|
if (!def || typeof def !== "object") return;
|
|
729
830
|
if (def.state) {
|
|
730
831
|
for (const [key, d] of Object.entries(def.state)) {
|
|
731
|
-
const entry = /** @type {
|
|
832
|
+
const entry = /** @type {JxMutableNode} */ (d);
|
|
732
833
|
if (
|
|
733
834
|
entry &&
|
|
734
835
|
typeof entry === "object" &&
|
|
@@ -742,7 +843,9 @@ function _walkServerEntries(def, entries) {
|
|
|
742
843
|
}
|
|
743
844
|
}
|
|
744
845
|
if (Array.isArray(def.children)) {
|
|
745
|
-
def.children.forEach((/** @type {
|
|
846
|
+
def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
847
|
+
_walkServerEntries(c, entries),
|
|
848
|
+
);
|
|
746
849
|
}
|
|
747
850
|
}
|
|
748
851
|
|
|
@@ -754,8 +857,8 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
|
|
|
754
857
|
/**
|
|
755
858
|
* Recursively render a Jx node tree to static HTML for pre-rendering.
|
|
756
859
|
*
|
|
757
|
-
* @param {
|
|
758
|
-
* @param {
|
|
860
|
+
* @param {JxElement | JxMutableNode | string} node
|
|
861
|
+
* @param {Record<string, unknown>} scope
|
|
759
862
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
760
863
|
* @returns {string}
|
|
761
864
|
*/
|
|
@@ -769,7 +872,11 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
769
872
|
}
|
|
770
873
|
if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
|
|
771
874
|
if (Array.isArray(node))
|
|
772
|
-
return node
|
|
875
|
+
return node
|
|
876
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
877
|
+
renderStaticNode(c, scope, slotContent),
|
|
878
|
+
)
|
|
879
|
+
.join("\n");
|
|
773
880
|
if (!node || typeof node !== "object") return "";
|
|
774
881
|
|
|
775
882
|
// Skip mapped arrays — can't pre-render dynamic lists
|
|
@@ -790,10 +897,12 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
790
897
|
inner = val != null ? escapeHtml(String(val)) : "";
|
|
791
898
|
} else if (node.innerHTML) {
|
|
792
899
|
const val = resolveStaticValue(node.innerHTML, scope);
|
|
793
|
-
inner = val != null ? val : node.innerHTML;
|
|
900
|
+
inner = val != null ? String(val) : /** @type {string} */ (node.innerHTML);
|
|
794
901
|
} else if (Array.isArray(node.children)) {
|
|
795
902
|
inner = node.children
|
|
796
|
-
.map((/** @type {
|
|
903
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
904
|
+
renderStaticNode(c, scope, slotContent),
|
|
905
|
+
)
|
|
797
906
|
.join("\n");
|
|
798
907
|
}
|
|
799
908
|
|
|
@@ -803,39 +912,41 @@ export function renderStaticNode(node, scope, slotContent = null) {
|
|
|
803
912
|
/**
|
|
804
913
|
* Pre-render a component definition to static HTML for its inner content.
|
|
805
914
|
*
|
|
806
|
-
* @param {
|
|
807
|
-
* @param {Record<string,
|
|
808
|
-
* state
|
|
915
|
+
* @param {JxElement} doc - Component JSON definition
|
|
916
|
+
* @param {Record<string, unknown> | null} [propsOverride] - Instance-specific prop values to merge
|
|
917
|
+
* into state
|
|
809
918
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
810
919
|
* @returns {string} The pre-rendered innerHTML
|
|
811
920
|
*/
|
|
812
921
|
export function preRenderComponentHtml(doc, propsOverride = null, slotContent = null) {
|
|
922
|
+
/** @type {Record<string, JxStateDefinition>} */
|
|
813
923
|
let stateDefs = doc.state ?? {};
|
|
814
924
|
if (propsOverride) {
|
|
815
925
|
stateDefs = { ...stateDefs };
|
|
816
926
|
for (const [key, value] of Object.entries(propsOverride)) {
|
|
817
927
|
if (key in stateDefs) {
|
|
818
928
|
const existing = stateDefs[key];
|
|
819
|
-
// If the existing definition is an expanded signal with "default", override the default
|
|
820
929
|
if (
|
|
821
930
|
existing &&
|
|
822
931
|
typeof existing === "object" &&
|
|
823
932
|
!Array.isArray(existing) &&
|
|
824
933
|
"default" in existing
|
|
825
934
|
) {
|
|
826
|
-
stateDefs[key] = {
|
|
935
|
+
stateDefs[key] = { .../** @type {JxStateObject} */ (existing), default: value };
|
|
827
936
|
} else {
|
|
828
|
-
stateDefs[key] = value;
|
|
937
|
+
stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
829
938
|
}
|
|
830
939
|
} else {
|
|
831
|
-
stateDefs[key] = value;
|
|
940
|
+
stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
832
941
|
}
|
|
833
942
|
}
|
|
834
943
|
}
|
|
835
944
|
const scope = buildInitialScope(stateDefs, null);
|
|
836
945
|
if (!Array.isArray(doc.children)) return "";
|
|
837
946
|
return doc.children
|
|
838
|
-
.map((/** @type {
|
|
947
|
+
.map((/** @type {JxElement | JxMutableNode | string} */ c) =>
|
|
948
|
+
renderStaticNode(c, scope, slotContent),
|
|
949
|
+
)
|
|
839
950
|
.join("\n");
|
|
840
951
|
}
|
|
841
952
|
|
|
@@ -845,7 +956,7 @@ export function preRenderComponentHtml(doc, propsOverride = null, slotContent =
|
|
|
845
956
|
* Returns true when: no event handlers, no $prototype entries (Functions, Request, Storage), no
|
|
846
957
|
* $ref values. Conservative — returns false when uncertain.
|
|
847
958
|
*
|
|
848
|
-
* @param {
|
|
959
|
+
* @param {JxElement} doc - Component JSON definition
|
|
849
960
|
* @returns {boolean}
|
|
850
961
|
*/
|
|
851
962
|
export function isComponentFullyStatic(doc) {
|
|
@@ -853,7 +964,7 @@ export function isComponentFullyStatic(doc) {
|
|
|
853
964
|
}
|
|
854
965
|
|
|
855
966
|
/**
|
|
856
|
-
* @param {
|
|
967
|
+
* @param {JxElement | string | (JxElement | string)[]} node
|
|
857
968
|
* @returns {boolean}
|
|
858
969
|
*/
|
|
859
970
|
function _isStaticNode(node) {
|
|
@@ -869,7 +980,7 @@ function _isStaticNode(node) {
|
|
|
869
980
|
if (node.state) {
|
|
870
981
|
for (const def of Object.values(node.state)) {
|
|
871
982
|
if (!def || typeof def !== "object") continue;
|
|
872
|
-
const d = /** @type {
|
|
983
|
+
const d = /** @type {JxMutableNode} */ (def);
|
|
873
984
|
if (d.$prototype) return false;
|
|
874
985
|
if (d.$ref) return false;
|
|
875
986
|
}
|
|
@@ -896,9 +1007,9 @@ function _isStaticNode(node) {
|
|
|
896
1007
|
* styles using .jx-N selectors via collectStyles.
|
|
897
1008
|
*
|
|
898
1009
|
* @param {string} tagName - The custom element tag name (used as CSS selector)
|
|
899
|
-
* @param {
|
|
900
|
-
* @param {
|
|
901
|
-
* @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
|
|
902
1013
|
* @returns {string} CSS text, or empty string if no styles
|
|
903
1014
|
*/
|
|
904
1015
|
export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
|
|
@@ -930,7 +1041,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
|
|
|
930
1041
|
const query = prop.startsWith("@--")
|
|
931
1042
|
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
932
1043
|
: prop.slice(1);
|
|
933
|
-
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {
|
|
1044
|
+
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
|
|
934
1045
|
} else if (
|
|
935
1046
|
prop.startsWith(":") ||
|
|
936
1047
|
prop.startsWith(".") ||
|
|
@@ -938,7 +1049,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
|
|
|
938
1049
|
prop.startsWith("[")
|
|
939
1050
|
) {
|
|
940
1051
|
const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
|
|
941
|
-
rules.push(`${resolved} { ${toCSSText(/** @type {
|
|
1052
|
+
rules.push(`${resolved} { ${toCSSText(/** @type {object} */ (val))} }`);
|
|
942
1053
|
}
|
|
943
1054
|
}
|
|
944
1055
|
}
|