@jxsuite/compiler 0.0.1
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 +165 -0
- package/package.json +38 -0
- package/src/cli.js +59 -0
- package/src/compiler.js +148 -0
- package/src/shared.js +690 -0
- package/src/site/content-loader.js +452 -0
- package/src/site/context-injection.js +152 -0
- package/src/site/head-merger.js +161 -0
- package/src/site/layout-resolver.js +182 -0
- package/src/site/pages-discovery.js +272 -0
- package/src/site/prototype-resolver.js +161 -0
- package/src/site/site-build.js +600 -0
- package/src/site/site-loader.js +85 -0
- package/src/targets/compile-class.js +194 -0
- package/src/targets/compile-client.js +806 -0
- package/src/targets/compile-element.js +619 -0
- package/src/targets/compile-server.js +57 -0
- package/src/targets/compile-static.js +155 -0
package/src/shared.js
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared.js — Shared compiler utilities
|
|
3
|
+
*
|
|
4
|
+
* Detection, scope resolution, HTML building, CSS extraction, and naming utilities used across all
|
|
5
|
+
* compilation targets (static, client, element, server).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { camelToKebab, toCSSText, RESERVED_KEYS } from "@jxsuite/runtime";
|
|
9
|
+
|
|
10
|
+
// Re-export runtime utilities used by submodules
|
|
11
|
+
export { camelToKebab, toCSSText, RESERVED_KEYS };
|
|
12
|
+
|
|
13
|
+
// CDN defaults
|
|
14
|
+
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.32";
|
|
15
|
+
export const DEFAULT_LIT_HTML_SRC = "https://esm.sh/lit-html@3.3.0";
|
|
16
|
+
|
|
17
|
+
// ─── Schema keywords ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Schema-only keywords used to detect pure type definitions (Shape 2b). An object with ONLY these
|
|
21
|
+
* keys and no `default` is a type def, not a signal.
|
|
22
|
+
*/
|
|
23
|
+
export const SCHEMA_KEYWORDS = new Set([
|
|
24
|
+
"type",
|
|
25
|
+
"enum",
|
|
26
|
+
"minimum",
|
|
27
|
+
"maximum",
|
|
28
|
+
"minLength",
|
|
29
|
+
"maxLength",
|
|
30
|
+
"pattern",
|
|
31
|
+
"format",
|
|
32
|
+
"items",
|
|
33
|
+
"properties",
|
|
34
|
+
"required",
|
|
35
|
+
"description",
|
|
36
|
+
"title",
|
|
37
|
+
"$comment",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if a $src path points to a .class.json schema-defined class.
|
|
44
|
+
*
|
|
45
|
+
* @param {any} src
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isClassJsonSrc(src) {
|
|
49
|
+
return typeof src === "string" && src.endsWith(".class.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if an object contains only schema keywords (no `default`, no `$prototype`).
|
|
54
|
+
*
|
|
55
|
+
* @param {any} obj
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function isSchemaOnly(obj) {
|
|
59
|
+
for (const k of Object.keys(obj)) {
|
|
60
|
+
if (!SCHEMA_KEYWORDS.has(k)) return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if a string contains a ${} template expression.
|
|
67
|
+
*
|
|
68
|
+
* @param {any} val
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isTemplateString(val) {
|
|
72
|
+
return typeof val === "string" && val.includes("${");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determine whether a node (or any of its descendants) requires client-side JavaScript.
|
|
77
|
+
*
|
|
78
|
+
* @param {any} def
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function isDynamic(def) {
|
|
82
|
+
if (!def || typeof def !== "object") return false;
|
|
83
|
+
|
|
84
|
+
if (def.state) {
|
|
85
|
+
for (const [k, d] of Object.entries(def.state)) {
|
|
86
|
+
// Skip injected context (read-only, not reactive)
|
|
87
|
+
if (k === "$site" || k === "$page") continue;
|
|
88
|
+
// Skip timing: "compiler" entries — resolved at build time, baked into static HTML
|
|
89
|
+
if (
|
|
90
|
+
d &&
|
|
91
|
+
typeof d === "object" &&
|
|
92
|
+
!Array.isArray(d) &&
|
|
93
|
+
/** @type {any} */ (d).timing === "compiler"
|
|
94
|
+
)
|
|
95
|
+
continue;
|
|
96
|
+
if (typeof d !== "object" || d === null || Array.isArray(d)) return true;
|
|
97
|
+
if (/** @type {any} */ (d).$prototype) return true;
|
|
98
|
+
if ("default" in /** @type {any} */ (d)) return true;
|
|
99
|
+
if (isSchemaOnly(d)) continue;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (def.$switch) return true;
|
|
105
|
+
if (def.children?.$prototype === "Array") return true;
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(def.children)) {
|
|
108
|
+
if (def.children.some(/** @param {any} c */ (c) => isDynamic(c))) return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const [key, val] of Object.entries(def)) {
|
|
112
|
+
if (RESERVED_KEYS.has(key)) continue;
|
|
113
|
+
if (
|
|
114
|
+
val !== null &&
|
|
115
|
+
typeof val === "object" &&
|
|
116
|
+
typeof (/** @type {any} */ (val).$ref) === "string"
|
|
117
|
+
)
|
|
118
|
+
return true;
|
|
119
|
+
if (isTemplateString(val)) return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (def.style && typeof def.style === "object") {
|
|
123
|
+
for (const val of Object.values(def.style)) {
|
|
124
|
+
if (isTemplateString(val)) return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (def.attributes && typeof def.attributes === "object") {
|
|
129
|
+
for (const val of Object.values(def.attributes)) {
|
|
130
|
+
if (isTemplateString(val)) return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shallow variant of isDynamic — checks only this node's own properties, not its children.
|
|
139
|
+
*
|
|
140
|
+
* @param {any} def
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
export function isNodeDynamic(def) {
|
|
144
|
+
if (!def || typeof def !== "object") return false;
|
|
145
|
+
|
|
146
|
+
if (def.$switch) return true;
|
|
147
|
+
if (def.children?.$prototype === "Array") return true;
|
|
148
|
+
|
|
149
|
+
for (const [key, val] of Object.entries(def)) {
|
|
150
|
+
if (RESERVED_KEYS.has(key)) continue;
|
|
151
|
+
if (
|
|
152
|
+
val !== null &&
|
|
153
|
+
typeof val === "object" &&
|
|
154
|
+
typeof (/** @type {any} */ (val).$ref) === "string"
|
|
155
|
+
)
|
|
156
|
+
return true;
|
|
157
|
+
if (isTemplateString(val)) return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (def.style && typeof def.style === "object") {
|
|
161
|
+
for (const val of Object.values(def.style)) {
|
|
162
|
+
if (isTemplateString(val)) return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (def.attributes && typeof def.attributes === "object") {
|
|
167
|
+
for (const val of Object.values(def.attributes)) {
|
|
168
|
+
if (isTemplateString(val)) return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns true if any node in the tree will need dynamic handling.
|
|
177
|
+
*
|
|
178
|
+
* @param {any} def
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
export function hasAnyIsland(def) {
|
|
182
|
+
if (!def || typeof def !== "object") return false;
|
|
183
|
+
if (isDynamic(def)) return true;
|
|
184
|
+
if (Array.isArray(def.children))
|
|
185
|
+
return def.children.some(/** @param {any} c */ (c) => hasAnyIsland(c));
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Scope / value resolution ─────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {any} raw
|
|
193
|
+
* @param {any} [parentScope]
|
|
194
|
+
* @param {Record<string, any>} [scopeDefs]
|
|
195
|
+
* @param {Record<string, any>} [media]
|
|
196
|
+
* @returns {{ scope: any; scopeDefs: Record<string, any>; media: Record<string, any> }}
|
|
197
|
+
*/
|
|
198
|
+
export function createCompileContext(raw, parentScope = null, scopeDefs = {}, media = {}) {
|
|
199
|
+
const scope = raw?.state
|
|
200
|
+
? buildInitialScope(raw.state, parentScope)
|
|
201
|
+
: (parentScope ?? Object.create(null));
|
|
202
|
+
return { scope, scopeDefs, media };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {Record<string, any>} [defs]
|
|
207
|
+
* @param {any} [parentScope]
|
|
208
|
+
* @returns {any}
|
|
209
|
+
*/
|
|
210
|
+
export function buildInitialScope(defs = {}, parentScope = null) {
|
|
211
|
+
const scope = Object.create(parentScope ?? null);
|
|
212
|
+
|
|
213
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
214
|
+
if (typeof def !== "object" || def === null || Array.isArray(def)) {
|
|
215
|
+
setOwnScopeValue(scope, key, cloneValue(def));
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if ("default" in def) {
|
|
219
|
+
setOwnScopeValue(scope, key, cloneValue(def.default));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!def.$prototype && !isSchemaOnly(def)) {
|
|
223
|
+
setOwnScopeValue(scope, key, cloneValue(def));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
228
|
+
if (typeof def === "string" && isTemplateString(def)) {
|
|
229
|
+
defineLazyScopeValue(scope, key, () => evaluateStaticTemplate(def, scope));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!def || typeof def !== "object") continue;
|
|
233
|
+
if (def.$prototype === "Function") {
|
|
234
|
+
if (def.body) {
|
|
235
|
+
const fn = new Function("state", ...(def.parameters ?? def.arguments ?? []), def.body);
|
|
236
|
+
if (def.body.includes("return")) {
|
|
237
|
+
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
238
|
+
} else {
|
|
239
|
+
setOwnScopeValue(scope, key, fn);
|
|
240
|
+
}
|
|
241
|
+
} else if (!def.body?.includes("return")) {
|
|
242
|
+
setOwnScopeValue(scope, key, () => {});
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
|
|
247
|
+
setOwnScopeValue(scope, key, cloneValue(def.default ?? null));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return scope;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {any} scope
|
|
256
|
+
* @param {string} key
|
|
257
|
+
* @param {any} value
|
|
258
|
+
*/
|
|
259
|
+
export function setOwnScopeValue(scope, key, value) {
|
|
260
|
+
Object.defineProperty(scope, key, {
|
|
261
|
+
value,
|
|
262
|
+
enumerable: true,
|
|
263
|
+
configurable: true,
|
|
264
|
+
writable: true,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {any} scope
|
|
270
|
+
* @param {string} key
|
|
271
|
+
* @param {() => any} getter
|
|
272
|
+
*/
|
|
273
|
+
export function defineLazyScopeValue(scope, key, getter) {
|
|
274
|
+
Object.defineProperty(scope, key, {
|
|
275
|
+
enumerable: true,
|
|
276
|
+
configurable: true,
|
|
277
|
+
get: getter,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {any} value
|
|
283
|
+
* @param {any} scope
|
|
284
|
+
* @returns {any}
|
|
285
|
+
*/
|
|
286
|
+
export function resolveStaticValue(value, scope) {
|
|
287
|
+
if (isRefObject(value)) return resolveRefValue(value.$ref, scope);
|
|
288
|
+
if (isTemplateString(value)) return evaluateStaticTemplate(value, scope);
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {any} value
|
|
294
|
+
* @returns {boolean}
|
|
295
|
+
*/
|
|
296
|
+
export function isRefObject(value) {
|
|
297
|
+
return value !== null && typeof value === "object" && typeof value.$ref === "string";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {any} refValue
|
|
302
|
+
* @param {any} scope
|
|
303
|
+
* @returns {any}
|
|
304
|
+
*/
|
|
305
|
+
export function resolveRefValue(refValue, scope) {
|
|
306
|
+
if (typeof refValue !== "string") return refValue;
|
|
307
|
+
if (refValue.startsWith("$map/")) {
|
|
308
|
+
const parts = refValue.split("/");
|
|
309
|
+
const key = parts[1];
|
|
310
|
+
const base = scope.$map?.[key] ?? scope["$map/" + key];
|
|
311
|
+
return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
|
|
312
|
+
}
|
|
313
|
+
if (refValue.startsWith("#/state/")) {
|
|
314
|
+
const sub = refValue.slice("#/state/".length);
|
|
315
|
+
const slash = sub.indexOf("/");
|
|
316
|
+
if (slash < 0) return scope[sub];
|
|
317
|
+
return getPathValue(scope[sub.slice(0, slash)], sub.slice(slash + 1));
|
|
318
|
+
}
|
|
319
|
+
return scope[refValue] ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} str
|
|
324
|
+
* @param {any} scope
|
|
325
|
+
* @returns {any}
|
|
326
|
+
*/
|
|
327
|
+
export function evaluateStaticTemplate(str, scope) {
|
|
328
|
+
const fn = new Function("state", "$map", `return \`${str}\``);
|
|
329
|
+
return fn(scope, scope?.$map);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {any} base
|
|
334
|
+
* @param {string} path
|
|
335
|
+
* @returns {any}
|
|
336
|
+
*/
|
|
337
|
+
export function getPathValue(base, path) {
|
|
338
|
+
if (!path) return base;
|
|
339
|
+
return path
|
|
340
|
+
.split("/")
|
|
341
|
+
.reduce(
|
|
342
|
+
(/** @type {any} */ acc, /** @type {string} */ key) => (acc == null ? undefined : acc[key]),
|
|
343
|
+
base,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {any} value
|
|
349
|
+
* @returns {any}
|
|
350
|
+
*/
|
|
351
|
+
export function cloneValue(value) {
|
|
352
|
+
if (value === null || typeof value !== "object") return value;
|
|
353
|
+
return JSON.parse(JSON.stringify(value));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── HTML building ────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Build an HTML attribute string from a static element definition.
|
|
360
|
+
*
|
|
361
|
+
* @param {any} def
|
|
362
|
+
* @param {any} scope
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
365
|
+
export function buildAttrs(def, scope) {
|
|
366
|
+
let out = "";
|
|
367
|
+
|
|
368
|
+
const id = resolveStaticValue(def.id, scope);
|
|
369
|
+
const className = resolveStaticValue(def.className, scope);
|
|
370
|
+
const hidden = resolveStaticValue(def.hidden, scope);
|
|
371
|
+
const tabIndex = resolveStaticValue(def.tabIndex, scope);
|
|
372
|
+
const title = resolveStaticValue(def.title, scope);
|
|
373
|
+
const lang = resolveStaticValue(def.lang, scope);
|
|
374
|
+
const dir = resolveStaticValue(def.dir, scope);
|
|
375
|
+
|
|
376
|
+
if (id) out += ` id="${escapeHtml(id)}"`;
|
|
377
|
+
if (className) out += ` class="${escapeHtml(className)}"`;
|
|
378
|
+
if (hidden) out += " hidden";
|
|
379
|
+
if (tabIndex !== undefined && tabIndex !== null)
|
|
380
|
+
out += ` tabindex="${escapeHtml(String(tabIndex))}"`;
|
|
381
|
+
if (title) out += ` title="${escapeHtml(title)}"`;
|
|
382
|
+
if (lang) out += ` lang="${escapeHtml(lang)}"`;
|
|
383
|
+
if (dir) out += ` dir="${escapeHtml(dir)}"`;
|
|
384
|
+
|
|
385
|
+
if (def.style) {
|
|
386
|
+
// Collect properties that have @media overrides — these must NOT be inline
|
|
387
|
+
// because inline styles (specificity 1,0,0,0) always beat stylesheet @media rules.
|
|
388
|
+
const mediaOverriddenProps = new Set();
|
|
389
|
+
for (const [k, v] of Object.entries(def.style)) {
|
|
390
|
+
if (k.startsWith("@") && v && typeof v === "object") {
|
|
391
|
+
for (const prop of Object.keys(/** @type {Record<string, any>} */ (v))) {
|
|
392
|
+
if (
|
|
393
|
+
!prop.startsWith(":") &&
|
|
394
|
+
!prop.startsWith(".") &&
|
|
395
|
+
!prop.startsWith("&") &&
|
|
396
|
+
!prop.startsWith("[")
|
|
397
|
+
) {
|
|
398
|
+
mediaOverriddenProps.add(prop);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const inline = Object.entries(def.style)
|
|
405
|
+
.filter(
|
|
406
|
+
([k, v]) =>
|
|
407
|
+
!k.startsWith(":") &&
|
|
408
|
+
!k.startsWith(".") &&
|
|
409
|
+
!k.startsWith("&") &&
|
|
410
|
+
!k.startsWith("[") &&
|
|
411
|
+
!k.startsWith("@") &&
|
|
412
|
+
!mediaOverriddenProps.has(k) &&
|
|
413
|
+
v !== null &&
|
|
414
|
+
typeof v !== "object",
|
|
415
|
+
)
|
|
416
|
+
.map(([k, v]) => {
|
|
417
|
+
const value = resolveStaticValue(v, scope);
|
|
418
|
+
return value == null ? null : `${camelToKebab(k)}: ${value}`;
|
|
419
|
+
})
|
|
420
|
+
.filter(Boolean)
|
|
421
|
+
.join("; ");
|
|
422
|
+
if (inline) out += ` style="${inline}"`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (def.attributes) {
|
|
426
|
+
for (const [k, v] of Object.entries(def.attributes)) {
|
|
427
|
+
const value = resolveStaticValue(v, scope);
|
|
428
|
+
if (
|
|
429
|
+
value !== null &&
|
|
430
|
+
value !== undefined &&
|
|
431
|
+
(typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
432
|
+
) {
|
|
433
|
+
out += ` ${k}="${escapeHtml(String(value))}"`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build the inner HTML (textContent or children) for a node.
|
|
443
|
+
*
|
|
444
|
+
* @param {any} def
|
|
445
|
+
* @param {any} raw
|
|
446
|
+
* @param {{ scope: any; scopeDefs: Record<string, any>; media: Record<string, any> }} context
|
|
447
|
+
* @param {(def: any, raw: any, context: any) => string} childCompiler
|
|
448
|
+
* @returns {string}
|
|
449
|
+
*/
|
|
450
|
+
export function buildInner(def, raw, context, childCompiler) {
|
|
451
|
+
const source = raw ?? def;
|
|
452
|
+
|
|
453
|
+
if (source.textContent !== undefined) {
|
|
454
|
+
const value = resolveStaticValue(source.textContent, context.scope);
|
|
455
|
+
return value == null ? "" : escapeHtml(String(value));
|
|
456
|
+
}
|
|
457
|
+
if (source.innerHTML) return resolveStaticValue(source.innerHTML, context.scope) ?? "";
|
|
458
|
+
if (Array.isArray(source.children)) {
|
|
459
|
+
const rawChildren = raw?.children;
|
|
460
|
+
return source.children
|
|
461
|
+
.map((/** @type {any} */ c, /** @type {number} */ i) => {
|
|
462
|
+
const childRaw = rawChildren?.[i] ?? c;
|
|
463
|
+
return childCompiler(c, childRaw, context);
|
|
464
|
+
})
|
|
465
|
+
.join("\n ");
|
|
466
|
+
}
|
|
467
|
+
return "";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── CSS extraction ───────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Walk the entire document tree and collect all static nested CSS rules.
|
|
474
|
+
*
|
|
475
|
+
* @param {any} doc
|
|
476
|
+
* @param {Record<string, any>} [mediaQueries]
|
|
477
|
+
* @returns {string}
|
|
478
|
+
*/
|
|
479
|
+
export function compileStyles(doc, mediaQueries = {}) {
|
|
480
|
+
/** @type {string[]} */
|
|
481
|
+
const rules = [];
|
|
482
|
+
collectStyles(doc, rules, mediaQueries, "");
|
|
483
|
+
if (rules.length === 0) return "";
|
|
484
|
+
return `<style>\n${rules.join("\n")}\n</style>`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @param {any} def
|
|
489
|
+
* @param {string[]} rules
|
|
490
|
+
* @param {Record<string, any>} mediaQueries
|
|
491
|
+
* @param {string} [_parentSel]
|
|
492
|
+
*/
|
|
493
|
+
export function collectStyles(def, rules, mediaQueries, _parentSel = "") {
|
|
494
|
+
if (!def || typeof def !== "object") return;
|
|
495
|
+
|
|
496
|
+
const selector = def.id
|
|
497
|
+
? `#${def.id}`
|
|
498
|
+
: def.className
|
|
499
|
+
? `.${def.className.split(" ")[0]}`
|
|
500
|
+
: (def.tagName ?? "*");
|
|
501
|
+
|
|
502
|
+
if (def.style) {
|
|
503
|
+
// Collect properties that have @media overrides — these are excluded from
|
|
504
|
+
// inline styles in buildAttrs(), so we emit them as base CSS rules here.
|
|
505
|
+
const mediaOverriddenProps = new Set();
|
|
506
|
+
for (const [k, v] of Object.entries(def.style)) {
|
|
507
|
+
if (k.startsWith("@") && v && typeof v === "object") {
|
|
508
|
+
for (const p of Object.keys(/** @type {Record<string, any>} */ (v))) {
|
|
509
|
+
if (
|
|
510
|
+
!p.startsWith(":") &&
|
|
511
|
+
!p.startsWith(".") &&
|
|
512
|
+
!p.startsWith("&") &&
|
|
513
|
+
!p.startsWith("[")
|
|
514
|
+
) {
|
|
515
|
+
mediaOverriddenProps.add(p);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Emit base CSS rules for media-overridden properties
|
|
522
|
+
if (mediaOverriddenProps.size > 0) {
|
|
523
|
+
const baseDecls = [];
|
|
524
|
+
for (const p of mediaOverriddenProps) {
|
|
525
|
+
const v = def.style[p];
|
|
526
|
+
if (v !== null && v !== undefined && typeof v !== "object") {
|
|
527
|
+
baseDecls.push(`${camelToKebab(p)}: ${v}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (baseDecls.length > 0) {
|
|
531
|
+
rules.push(`${selector} { ${baseDecls.join("; ")} }`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
for (const [prop, val] of Object.entries(def.style)) {
|
|
536
|
+
if (prop.startsWith("@")) {
|
|
537
|
+
const query = prop.startsWith("@--")
|
|
538
|
+
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
539
|
+
: prop.slice(1);
|
|
540
|
+
rules.push(`@media ${query} { ${selector} { ${toCSSText(/** @type {any} */ (val))} } }`);
|
|
541
|
+
for (const [sel, nestedRules] of Object.entries(/** @type {Record<string, any>} */ (val))) {
|
|
542
|
+
if (
|
|
543
|
+
!(
|
|
544
|
+
sel.startsWith(":") ||
|
|
545
|
+
sel.startsWith(".") ||
|
|
546
|
+
sel.startsWith("&") ||
|
|
547
|
+
sel.startsWith("[")
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
continue;
|
|
551
|
+
const resolved = sel.startsWith("&") ? sel.replace("&", selector) : `${selector}${sel}`;
|
|
552
|
+
rules.push(
|
|
553
|
+
`@media ${query} { ${resolved} { ${toCSSText(/** @type {any} */ (nestedRules))} } }`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
} else if (
|
|
557
|
+
prop.startsWith(":") ||
|
|
558
|
+
prop.startsWith(".") ||
|
|
559
|
+
prop.startsWith("&") ||
|
|
560
|
+
prop.startsWith("[")
|
|
561
|
+
) {
|
|
562
|
+
const resolved = prop.startsWith("&") ? prop.replace("&", selector) : `${selector}${prop}`;
|
|
563
|
+
rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (Array.isArray(def.children)) {
|
|
569
|
+
def.children.forEach((/** @type {any} */ c) => {
|
|
570
|
+
collectStyles(c, rules, mediaQueries, selector);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* HTML-escape a string for safe attribute and text content embedding.
|
|
579
|
+
*
|
|
580
|
+
* @param {string} str
|
|
581
|
+
* @returns {string}
|
|
582
|
+
*/
|
|
583
|
+
export function escapeHtml(str) {
|
|
584
|
+
return String(str)
|
|
585
|
+
.replace(/&/g, "&")
|
|
586
|
+
.replace(/</g, "<")
|
|
587
|
+
.replace(/>/g, ">")
|
|
588
|
+
.replace(/"/g, """)
|
|
589
|
+
.replace(/'/g, "'");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Convert a page title to a valid custom element tag name.
|
|
594
|
+
*
|
|
595
|
+
* @param {string} title
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
export function titleToTagName(title) {
|
|
599
|
+
const slug = title
|
|
600
|
+
.toLowerCase()
|
|
601
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
602
|
+
.replace(/^-|-$/g, "");
|
|
603
|
+
return slug.includes("-") ? slug : `jx-${slug}`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @param {string} tagName
|
|
608
|
+
* @returns {string}
|
|
609
|
+
*/
|
|
610
|
+
export function tagNameToClassName(tagName) {
|
|
611
|
+
return tagName
|
|
612
|
+
.split("-")
|
|
613
|
+
.map((/** @type {string} */ s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
614
|
+
.join("");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Recursively collect unique $src values from $prototype: "Function" entries.
|
|
619
|
+
*
|
|
620
|
+
* @param {any} doc
|
|
621
|
+
* @returns {string[]}
|
|
622
|
+
*/
|
|
623
|
+
export function collectSrcImports(doc) {
|
|
624
|
+
/** @type {Set<string>} */
|
|
625
|
+
const srcs = new Set();
|
|
626
|
+
_walkSrc(doc, srcs);
|
|
627
|
+
return [...srcs];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param {any} def
|
|
632
|
+
* @param {Set<string>} srcs
|
|
633
|
+
*/
|
|
634
|
+
function _walkSrc(def, srcs) {
|
|
635
|
+
if (!def || typeof def !== "object") return;
|
|
636
|
+
if (def.state) {
|
|
637
|
+
for (const d of Object.values(def.state)) {
|
|
638
|
+
if (
|
|
639
|
+
d &&
|
|
640
|
+
typeof d === "object" &&
|
|
641
|
+
/** @type {any} */ (d).$prototype === "Function" &&
|
|
642
|
+
/** @type {any} */ (d).$src
|
|
643
|
+
) {
|
|
644
|
+
srcs.add(/** @type {any} */ (d).$src);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (Array.isArray(def.children)) {
|
|
649
|
+
def.children.forEach((/** @type {any} */ c) => _walkSrc(c, srcs));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Recursively collect all `timing: "server"` entries from the document tree.
|
|
655
|
+
*
|
|
656
|
+
* @param {any} doc
|
|
657
|
+
* @returns {any[]}
|
|
658
|
+
*/
|
|
659
|
+
export function collectServerEntries(doc) {
|
|
660
|
+
/** @type {Map<string, any>} */
|
|
661
|
+
const entries = new Map();
|
|
662
|
+
_walkServerEntries(doc, entries);
|
|
663
|
+
return [...entries.values()];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* @param {any} def
|
|
668
|
+
* @param {Map<string, any>} entries
|
|
669
|
+
*/
|
|
670
|
+
function _walkServerEntries(def, entries) {
|
|
671
|
+
if (!def || typeof def !== "object") return;
|
|
672
|
+
if (def.state) {
|
|
673
|
+
for (const [key, d] of Object.entries(def.state)) {
|
|
674
|
+
const entry = /** @type {any} */ (d);
|
|
675
|
+
if (
|
|
676
|
+
entry &&
|
|
677
|
+
typeof entry === "object" &&
|
|
678
|
+
entry.timing === "server" &&
|
|
679
|
+
entry.$src &&
|
|
680
|
+
entry.$export &&
|
|
681
|
+
!entry.$prototype
|
|
682
|
+
) {
|
|
683
|
+
entries.set(entry.$export, { key, exportName: entry.$export, src: entry.$src });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (Array.isArray(def.children)) {
|
|
688
|
+
def.children.forEach((/** @type {any} */ c) => _walkServerEntries(c, entries));
|
|
689
|
+
}
|
|
690
|
+
}
|