@caelo-cms/shared 0.10.72 → 0.11.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/ai-tools.d.ts +125 -50
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +48 -36
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +110 -30
- package/dist/build-page.d.ts.map +1 -1
- package/dist/build-page.js +23 -4
- package/dist/build-page.js.map +1 -1
- package/dist/content.d.ts +201 -100
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +176 -74
- package/dist/content.js.map +1 -1
- package/dist/css-var-aliases.d.ts +20 -0
- package/dist/css-var-aliases.d.ts.map +1 -0
- package/dist/css-var-aliases.js +95 -0
- package/dist/css-var-aliases.js.map +1 -0
- package/dist/css-var-scan.d.ts +0 -16
- package/dist/css-var-scan.d.ts.map +1 -1
- package/dist/css-var-scan.js +24 -0
- package/dist/css-var-scan.js.map +1 -1
- package/dist/edit-markers.d.ts +41 -5
- package/dist/edit-markers.d.ts.map +1 -1
- package/dist/edit-markers.js +47 -50
- package/dist/edit-markers.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.js +2 -2
- package/dist/interactions.js.map +1 -1
- package/dist/link-canonicalise.d.ts.map +1 -1
- package/dist/link-canonicalise.js +7 -1
- package/dist/link-canonicalise.js.map +1 -1
- package/dist/link-refs.d.ts +1 -1
- package/dist/link-refs.js +3 -3
- package/dist/lists/address.d.ts +34 -0
- package/dist/lists/address.d.ts.map +1 -0
- package/dist/lists/address.js +43 -0
- package/dist/lists/address.js.map +1 -0
- package/dist/lists/default-types.d.ts +10 -0
- package/dist/lists/default-types.d.ts.map +1 -0
- package/dist/lists/default-types.js +91 -0
- package/dist/lists/default-types.js.map +1 -0
- package/dist/lists/fields.d.ts +152 -0
- package/dist/lists/fields.d.ts.map +1 -0
- package/dist/lists/fields.js +258 -0
- package/dist/lists/fields.js.map +1 -0
- package/dist/lists/index.d.ts +14 -0
- package/dist/lists/index.d.ts.map +1 -0
- package/dist/lists/index.js +15 -0
- package/dist/lists/index.js.map +1 -0
- package/dist/lists/item-attr.d.ts +26 -0
- package/dist/lists/item-attr.d.ts.map +1 -0
- package/dist/lists/item-attr.js +31 -0
- package/dist/lists/item-attr.js.map +1 -0
- package/dist/lists/items.d.ts +105 -0
- package/dist/lists/items.d.ts.map +1 -0
- package/dist/lists/items.js +304 -0
- package/dist/lists/items.js.map +1 -0
- package/dist/lists/list-type.d.ts +401 -0
- package/dist/lists/list-type.d.ts.map +1 -0
- package/dist/lists/list-type.js +275 -0
- package/dist/lists/list-type.js.map +1 -0
- package/dist/lists/model.d.ts +14 -0
- package/dist/lists/model.d.ts.map +1 -0
- package/dist/lists/model.js +15 -0
- package/dist/lists/model.js.map +1 -0
- package/dist/lists/page-type.d.ts +115 -0
- package/dist/lists/page-type.d.ts.map +1 -0
- package/dist/lists/page-type.js +66 -0
- package/dist/lists/page-type.js.map +1 -0
- package/dist/lists/refs.d.ts +13 -0
- package/dist/lists/refs.d.ts.map +1 -0
- package/dist/lists/refs.js +18 -0
- package/dist/lists/refs.js.map +1 -0
- package/dist/lists/render-data.d.ts +114 -0
- package/dist/lists/render-data.d.ts.map +1 -0
- package/dist/lists/render-data.js +3 -0
- package/dist/lists/render-data.js.map +1 -0
- package/dist/lists/render-deps.d.ts +68 -0
- package/dist/lists/render-deps.d.ts.map +1 -0
- package/dist/lists/render-deps.js +91 -0
- package/dist/lists/render-deps.js.map +1 -0
- package/dist/lists/resolve-values.d.ts +63 -0
- package/dist/lists/resolve-values.d.ts.map +1 -0
- package/dist/lists/resolve-values.js +181 -0
- package/dist/lists/resolve-values.js.map +1 -0
- package/dist/lists/resolve.d.ts +106 -0
- package/dist/lists/resolve.d.ts.map +1 -0
- package/dist/lists/resolve.js +878 -0
- package/dist/lists/resolve.js.map +1 -0
- package/dist/lists/scope.d.ts +81 -0
- package/dist/lists/scope.d.ts.map +1 -0
- package/dist/lists/scope.js +3 -0
- package/dist/lists/scope.js.map +1 -0
- package/dist/markup-structure.d.ts +8 -0
- package/dist/markup-structure.d.ts.map +1 -1
- package/dist/markup-structure.js +2 -0
- package/dist/markup-structure.js.map +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/nested-module-render.d.ts +25 -20
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +122 -31
- package/dist/nested-module-render.js.map +1 -1
- package/dist/page-log.d.ts +1 -1
- package/dist/preview-compose.d.ts +24 -21
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +34 -118
- package/dist/preview-compose.js.map +1 -1
- package/dist/source-text-transport.d.ts +17 -0
- package/dist/source-text-transport.d.ts.map +1 -0
- package/dist/source-text-transport.js +71 -0
- package/dist/source-text-transport.js.map +1 -0
- package/dist/template-engine.d.ts +44 -31
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +75 -174
- package/dist/template-engine.js.map +1 -1
- package/dist/template-sections.d.ts +69 -0
- package/dist/template-sections.d.ts.map +1 -0
- package/dist/template-sections.js +423 -0
- package/dist/template-sections.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +8 -1
- package/src/__tests__/redos-hardening.test.ts +50 -16
- package/src/ai-tools-add-module-modes.test.ts +50 -0
- package/src/ai-tools.test.ts +10 -0
- package/src/ai-tools.ts +51 -37
- package/src/build-page.test.ts +34 -3
- package/src/build-page.ts +26 -3
- package/src/content.test.ts +111 -3
- package/src/content.ts +186 -75
- package/src/css-var-aliases.test.ts +61 -0
- package/src/css-var-aliases.ts +109 -0
- package/src/css-var-scan.ts +9 -0
- package/src/edit-markers.test.ts +37 -21
- package/src/edit-markers.ts +52 -20
- package/src/index.ts +3 -1
- package/src/interactions.ts +2 -2
- package/src/link-canonicalise.test.ts +36 -6
- package/src/link-canonicalise.ts +6 -1
- package/src/link-refs.test.ts +66 -42
- package/src/link-refs.ts +3 -3
- package/src/lists/address.ts +75 -0
- package/src/lists/default-types.ts +102 -0
- package/src/lists/fields.ts +285 -0
- package/src/lists/index.ts +16 -0
- package/src/lists/item-attr.test.ts +36 -0
- package/src/lists/item-attr.ts +49 -0
- package/src/lists/items.ts +402 -0
- package/src/lists/list-fixtures.test.ts +302 -0
- package/src/lists/list-type.ts +306 -0
- package/src/lists/lists.test.ts +328 -0
- package/src/lists/model.ts +16 -0
- package/src/lists/page-type.ts +71 -0
- package/src/lists/refs.ts +21 -0
- package/src/lists/render-data.ts +125 -0
- package/src/lists/render-deps.test.ts +132 -0
- package/src/lists/render-deps.ts +144 -0
- package/src/lists/resolve-values.ts +224 -0
- package/src/lists/resolve.test.ts +295 -0
- package/src/lists/resolve.ts +1148 -0
- package/src/lists/scope.ts +69 -0
- package/src/lists-compose-parity.test.ts +262 -0
- package/src/markup-structure.ts +10 -0
- package/src/nested-module-render.test.ts +111 -77
- package/src/nested-module-render.ts +85 -48
- package/src/preview-compose.test.ts +55 -19
- package/src/preview-compose.ts +59 -154
- package/src/source-text-transport.test.ts +63 -0
- package/src/source-text-transport.ts +70 -0
- package/src/template-engine-lists.test.ts +553 -0
- package/src/template-engine.test.ts +62 -80
- package/src/template-engine.ts +102 -245
- package/src/template-sections.ts +500 -0
- package/src/version.ts +1 -1
- package/dist/structured-sets.d.ts +0 -70
- package/dist/structured-sets.d.ts.map +0 -1
- package/dist/structured-sets.js +0 -92
- package/dist/structured-sets.js.map +0 -1
- package/src/structured-sets.ts +0 -114
- package/src/template-engine-data-lists.test.ts +0 -90
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
* values[fieldName] is
|
|
35
35
|
* { moduleId, contentInstanceId }.
|
|
36
36
|
* {{#fieldName}}…{{/fieldName}} — section over a list field
|
|
37
|
-
* (kind =
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* (kind = list / module-list).
|
|
38
|
+
* A `list` iterates the inner
|
|
39
|
+
* template per entry (an entry's
|
|
40
|
+
* `{{>f}}` module field recurses
|
|
41
|
+
* here too); module-list ignores
|
|
41
42
|
* the inner and renders each
|
|
42
43
|
* element's nested module HTML
|
|
43
44
|
* via `renderInner` (recursion).
|
|
@@ -60,8 +61,8 @@
|
|
|
60
61
|
* of band so the page's <head>/<style> + footer scripts are stable.
|
|
61
62
|
*/
|
|
62
63
|
|
|
63
|
-
import type { ModuleFieldKind } from "./content.js";
|
|
64
64
|
import { isLinkFailureMarker, type LinkResolver } from "./link-refs.js";
|
|
65
|
+
import type { ListPageContext } from "./lists/resolve.js";
|
|
65
66
|
import { tagModuleId } from "./module-id-tag.js";
|
|
66
67
|
import { caeloMissingComment, renderTemplate, type TemplateField } from "./template-engine.js";
|
|
67
68
|
|
|
@@ -74,11 +75,7 @@ export interface NestedModuleResource {
|
|
|
74
75
|
readonly html: string;
|
|
75
76
|
readonly css: string;
|
|
76
77
|
readonly js: string;
|
|
77
|
-
readonly fields: readonly
|
|
78
|
-
readonly name: string;
|
|
79
|
-
readonly kind: ModuleFieldKind;
|
|
80
|
-
readonly default?: unknown;
|
|
81
|
-
}[];
|
|
78
|
+
readonly fields: readonly TemplateField[];
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
/** A content instance the recursion can render a nested module with. */
|
|
@@ -87,6 +84,13 @@ export interface NestedContentInstanceResource {
|
|
|
87
84
|
readonly moduleId: string;
|
|
88
85
|
readonly values: Record<string, unknown>;
|
|
89
86
|
readonly deletedAt: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The stored content instance these values belong to, when `id` is a
|
|
89
|
+
* render-local alias (the editor's edit-marked copy `edit:<key>`):
|
|
90
|
+
* local list entries are addressed by the real instance id (overlays,
|
|
91
|
+
* `data-caelo-list-item`). Absent = `id` itself.
|
|
92
|
+
*/
|
|
93
|
+
readonly listHolderId?: string;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/** The stored shape of a `module` field value / `module-list` element. */
|
|
@@ -159,11 +163,12 @@ interface RenderContext {
|
|
|
159
163
|
* active theme on this install (renderer emits loud-raw for any
|
|
160
164
|
* `{{theme_<slot>_url}}` placeholders). */
|
|
161
165
|
readonly themeAssets: NestedRenderThemeAssets | undefined;
|
|
162
|
-
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
readonly
|
|
166
|
-
|
|
166
|
+
/** The page's list context (`lists/resolve.ts`), threaded to every
|
|
167
|
+
* nested module so a list renders the same at any depth — a menu
|
|
168
|
+
* inside a nested chrome module included. */
|
|
169
|
+
readonly lists: ListPageContext | undefined;
|
|
170
|
+
/** Content instance whose values this level renders (local list address). */
|
|
171
|
+
readonly listHolder: string | null;
|
|
167
172
|
/** Epic #724 — resolves internal references in every nested module. */
|
|
168
173
|
readonly linkResolver: LinkResolver | undefined;
|
|
169
174
|
}
|
|
@@ -197,10 +202,7 @@ export function renderModuleWithContent(
|
|
|
197
202
|
contentInstanceId: string,
|
|
198
203
|
resolver: NestedRenderResolver,
|
|
199
204
|
themeAssets?: NestedRenderThemeAssets,
|
|
200
|
-
|
|
201
|
-
readonly dataLists: Readonly<Record<string, ReadonlyArray<Readonly<Record<string, string>>>>>;
|
|
202
|
-
readonly dormantDataLists: Readonly<Record<string, string>>;
|
|
203
|
-
},
|
|
205
|
+
lists?: ListPageContext,
|
|
204
206
|
linkResolver?: LinkResolver,
|
|
205
207
|
): NestedRenderResult {
|
|
206
208
|
const touched = new Set<string>();
|
|
@@ -215,13 +217,15 @@ export function renderModuleWithContent(
|
|
|
215
217
|
path: new Set<string>(),
|
|
216
218
|
depth: 0,
|
|
217
219
|
themeAssets,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
lists,
|
|
221
|
+
listHolder: null,
|
|
220
222
|
linkResolver,
|
|
221
223
|
});
|
|
222
224
|
return { html, touchedModuleIds: touched, missingSlots: missing, failures };
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
228
|
+
|
|
225
229
|
function renderInner(moduleId: string, contentInstanceId: string, ctx: RenderContext): string {
|
|
226
230
|
if (ctx.depth >= MAX_RECURSION_DEPTH) {
|
|
227
231
|
fail(ctx, `depth-limit:${moduleId}/${contentInstanceId}`);
|
|
@@ -263,8 +267,10 @@ function renderInner(moduleId: string, contentInstanceId: string, ctx: RenderCon
|
|
|
263
267
|
path,
|
|
264
268
|
depth: ctx.depth + 1,
|
|
265
269
|
themeAssets: ctx.themeAssets,
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
lists: ctx.lists,
|
|
271
|
+
// Variant content (`variant:<key>`) is no stored instance, so its
|
|
272
|
+
// local list entries carry no instance address.
|
|
273
|
+
listHolder: ci.listHolderId ?? (UUID_RE.test(ci.id) ? ci.id : null),
|
|
268
274
|
linkResolver: ctx.linkResolver,
|
|
269
275
|
};
|
|
270
276
|
|
|
@@ -286,6 +292,9 @@ function renderInner(moduleId: string, contentInstanceId: string, ctx: RenderCon
|
|
|
286
292
|
* HTML for element i. Malformed elements (non-NestedRef shape)
|
|
287
293
|
* resolve to `undefined` — the engine emits the existing
|
|
288
294
|
* `module-list-malformed:<name>[<i>]` marker.
|
|
295
|
+
* - `{{>f}}` inside an entry of list field `<name>`:
|
|
296
|
+
* `list:<name>/<itemId>/<f>`, with the ref passed alongside (the
|
|
297
|
+
* entry may come from a shared list, not from `values`).
|
|
289
298
|
*
|
|
290
299
|
* Failure-marker parity: every literal `missingSlots` string the
|
|
291
300
|
* legacy hand-rolled substitution emitted is preserved — half by the
|
|
@@ -308,8 +317,8 @@ function substituteWithRecursion(
|
|
|
308
317
|
// stays off the page. Memoised: `{{>name}}` twice renders it once.
|
|
309
318
|
const fieldByName = new Map(fields.map((f) => [f.name, f]));
|
|
310
319
|
const rendered = new Map<string, string | undefined>();
|
|
311
|
-
const partials = (key: string): string | undefined => {
|
|
312
|
-
if (!rendered.has(key)) rendered.set(key, renderPartial(key, fieldByName, values, ctx));
|
|
320
|
+
const partials = (key: string, ref?: NestedRefValue): string | undefined => {
|
|
321
|
+
if (!rendered.has(key)) rendered.set(key, renderPartial(key, ref, fieldByName, values, ctx));
|
|
313
322
|
return rendered.get(key);
|
|
314
323
|
};
|
|
315
324
|
|
|
@@ -319,8 +328,8 @@ function substituteWithRecursion(
|
|
|
319
328
|
contentValues: values,
|
|
320
329
|
partials,
|
|
321
330
|
themeAssets: ctx.themeAssets,
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
lists: ctx.lists,
|
|
332
|
+
listHolder: { contentInstanceId: ctx.listHolder },
|
|
324
333
|
linkResolver: ctx.linkResolver,
|
|
325
334
|
});
|
|
326
335
|
for (const m of result.missingSlots) {
|
|
@@ -335,18 +344,20 @@ function substituteWithRecursion(
|
|
|
335
344
|
}
|
|
336
345
|
|
|
337
346
|
/**
|
|
338
|
-
* Render the partial the engine asked for: `<name>` (a `module` field)
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* `
|
|
347
|
+
* Render the partial the engine asked for: `<name>` (a `module` field),
|
|
348
|
+
* `<name>__<i>` (element i of a `module-list`) or
|
|
349
|
+
* `list:<name>/<itemId>/<f>` (a list entry's module field, `ref` given).
|
|
350
|
+
* `undefined` when the value is not a well-formed ref — the engine then
|
|
351
|
+
* emits its own `module-ref-malformed` / `module-list-malformed` marker.
|
|
342
352
|
*/
|
|
343
353
|
function renderPartial(
|
|
344
354
|
key: string,
|
|
355
|
+
ref: NestedRefValue | undefined,
|
|
345
356
|
fieldByName: ReadonlyMap<string, TemplateField>,
|
|
346
357
|
values: Readonly<Record<string, unknown>>,
|
|
347
358
|
ctx: RenderContext,
|
|
348
359
|
): string | undefined {
|
|
349
|
-
const hit = nestedRefForPartial(key, fieldByName, values);
|
|
360
|
+
const hit = nestedRefForPartial(key, ref, fieldByName, values);
|
|
350
361
|
if (!hit) return undefined;
|
|
351
362
|
// #1027 — a nested module's root carries its own id, exactly like a
|
|
352
363
|
// page placement's (the composer tags those), so the live-edit overlay
|
|
@@ -366,9 +377,15 @@ function renderPartial(
|
|
|
366
377
|
*/
|
|
367
378
|
function nestedRefForPartial(
|
|
368
379
|
key: string,
|
|
380
|
+
ref: NestedRefValue | undefined,
|
|
369
381
|
fieldByName: ReadonlyMap<string, TemplateField>,
|
|
370
382
|
values: Readonly<Record<string, unknown>>,
|
|
371
383
|
): { readonly ref: NestedRefValue; readonly field: string; readonly step: string } | undefined {
|
|
384
|
+
if (key.startsWith(LIST_PARTIAL_PREFIX)) {
|
|
385
|
+
const [name, itemId, entryField] = key.slice(LIST_PARTIAL_PREFIX.length).split("/");
|
|
386
|
+
if (!name || !itemId || !entryField || !ref || !isNestedRef(ref)) return undefined;
|
|
387
|
+
return { ref, field: name, step: `${name}[${itemId}].${entryField}` };
|
|
388
|
+
}
|
|
372
389
|
const single = fieldByName.get(key);
|
|
373
390
|
if (single?.kind === "module") {
|
|
374
391
|
const ref = values[key];
|
|
@@ -387,6 +404,9 @@ function nestedRefForPartial(
|
|
|
387
404
|
return isNestedRef(el) ? { ref: el, field: name, step: `${name}[${i}]` } : undefined;
|
|
388
405
|
}
|
|
389
406
|
|
|
407
|
+
/** Partial-key prefix of a list entry's embedded module (`template-sections.ts`). */
|
|
408
|
+
const LIST_PARTIAL_PREFIX = "list:";
|
|
409
|
+
|
|
390
410
|
/** A nested ref a module's template dispatches (see {@link dispatchedNestedRefs}). */
|
|
391
411
|
export interface DispatchedNestedRef extends NestedRefValue {
|
|
392
412
|
/** The `module` / `module-list` field holding it. */
|
|
@@ -408,6 +428,7 @@ export function dispatchedNestedRefs(
|
|
|
408
428
|
html: string,
|
|
409
429
|
fields: readonly TemplateField[],
|
|
410
430
|
values: Readonly<Record<string, unknown>>,
|
|
431
|
+
lists?: ListPageContext,
|
|
411
432
|
): DispatchedNestedRef[] {
|
|
412
433
|
const fieldByName = new Map(fields.map((f) => [f.name, f]));
|
|
413
434
|
const out: DispatchedNestedRef[] = [];
|
|
@@ -416,8 +437,9 @@ export function dispatchedNestedRefs(
|
|
|
416
437
|
html,
|
|
417
438
|
fields,
|
|
418
439
|
contentValues: values,
|
|
419
|
-
|
|
420
|
-
|
|
440
|
+
...(lists ? { lists } : {}),
|
|
441
|
+
partials: (key, ref) => {
|
|
442
|
+
const hit = nestedRefForPartial(key, ref, fieldByName, values);
|
|
421
443
|
if (!hit) return undefined;
|
|
422
444
|
if (!asked.has(key)) {
|
|
423
445
|
asked.add(key);
|
|
@@ -461,8 +483,10 @@ export function renderPlacedModule(
|
|
|
461
483
|
resolver: NestedRenderResolver,
|
|
462
484
|
options: {
|
|
463
485
|
readonly themeAssets?: NestedRenderThemeAssets;
|
|
464
|
-
|
|
465
|
-
readonly
|
|
486
|
+
/** The page's list context (`lists/resolve.ts`). */
|
|
487
|
+
readonly lists?: ListPageContext;
|
|
488
|
+
/** The placement's stored content instance (local list address); absent for defaults. */
|
|
489
|
+
readonly contentInstanceId?: string;
|
|
466
490
|
/** Epic #724 — resolves internal references (see `link-refs.ts`). */
|
|
467
491
|
readonly linkResolver?: LinkResolver;
|
|
468
492
|
},
|
|
@@ -482,8 +506,8 @@ export function renderPlacedModule(
|
|
|
482
506
|
path: new Set<string>(),
|
|
483
507
|
depth: 1,
|
|
484
508
|
themeAssets: options.themeAssets,
|
|
485
|
-
|
|
486
|
-
|
|
509
|
+
lists: options.lists,
|
|
510
|
+
listHolder: options.contentInstanceId ?? null,
|
|
487
511
|
linkResolver: options.linkResolver,
|
|
488
512
|
});
|
|
489
513
|
return { html, touchedModuleIds: touched, missingSlots: missing, failures };
|
|
@@ -491,19 +515,32 @@ export function renderPlacedModule(
|
|
|
491
515
|
|
|
492
516
|
/**
|
|
493
517
|
* Walk a content_instance's values to find every nested-module reference
|
|
494
|
-
* it carries
|
|
495
|
-
*
|
|
518
|
+
* it carries: `module` / `module-list` values and the `module` fields of
|
|
519
|
+
* local list entries at any depth (`{ id, node, values, children }`).
|
|
520
|
+
* Used by the caller to pre-batch the modules + content_instances the
|
|
521
|
+
* page will need.
|
|
496
522
|
*/
|
|
497
|
-
export function collectNestedRefs(values: Record<string, unknown
|
|
523
|
+
export function collectNestedRefs(values: Readonly<Record<string, unknown>>): NestedRefValue[] {
|
|
498
524
|
const refs: NestedRefValue[] = [];
|
|
499
|
-
|
|
525
|
+
const visit = (v: unknown, depth: number): void => {
|
|
526
|
+
if (depth > MAX_COLLECT_DEPTH || typeof v !== "object" || v === null) return;
|
|
500
527
|
if (isNestedRef(v)) {
|
|
501
|
-
refs.push(v);
|
|
502
|
-
|
|
503
|
-
for (const el of v) {
|
|
504
|
-
if (isNestedRef(el)) refs.push(el);
|
|
505
|
-
}
|
|
528
|
+
refs.push({ moduleId: v.moduleId, contentInstanceId: v.contentInstanceId });
|
|
529
|
+
return;
|
|
506
530
|
}
|
|
507
|
-
|
|
531
|
+
if (Array.isArray(v)) {
|
|
532
|
+
for (const el of v) visit(el, depth + 1);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const entry = v as { values?: unknown; children?: unknown };
|
|
536
|
+
if (typeof entry.values === "object" && entry.values !== null) {
|
|
537
|
+
for (const inner of Object.values(entry.values)) visit(inner, depth + 1);
|
|
538
|
+
}
|
|
539
|
+
if (Array.isArray(entry.children)) visit(entry.children, depth + 1);
|
|
540
|
+
};
|
|
541
|
+
for (const v of Object.values(values)) visit(v, 0);
|
|
508
542
|
return refs;
|
|
509
543
|
}
|
|
544
|
+
|
|
545
|
+
/** Bound for {@link collectNestedRefs}: list depth (8) × two levels per entry, plus slack. */
|
|
546
|
+
const MAX_COLLECT_DEPTH = 24;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "bun:test";
|
|
4
4
|
import { INTERACTIONS } from "./interactions.js";
|
|
5
|
+
import { fixtureData, fixtureLinkIndex, ITEM, LIST } from "./lists/list-fixtures.test.js";
|
|
6
|
+
import { createListRenderContext } from "./lists/resolve.js";
|
|
5
7
|
import {
|
|
6
8
|
ComposeError,
|
|
7
9
|
composePagePreview,
|
|
@@ -19,6 +21,10 @@ const blankModule = {
|
|
|
19
21
|
js: "",
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
/** List context over the fixture site, rendered outside any page. */
|
|
25
|
+
const LISTS = createListRenderContext(fixtureData()).forPage(null);
|
|
26
|
+
const uid = (n: number) => `00000000-0000-4000-9000-${String(n).padStart(12, "0")}`;
|
|
27
|
+
|
|
22
28
|
describe("composePagePreview", () => {
|
|
23
29
|
it("inlines module HTML into named slots and stamps CSS/JS into head/body", () => {
|
|
24
30
|
const out = composePagePreview({
|
|
@@ -279,16 +285,17 @@ describe("composePagePreview substitutes {{name}} placeholders with field defaul
|
|
|
279
285
|
|
|
280
286
|
describe("composePagePreview list-iteration via shared template engine (#71)", () => {
|
|
281
287
|
// PR #71 / Plan B — the compose path now routes through the shared
|
|
282
|
-
// template engine and gains
|
|
283
|
-
//
|
|
288
|
+
// template engine and gains list / module-list section support
|
|
289
|
+
// (`list` since epic #1107). The fixtures below are the AC verbatim shapes;
|
|
284
290
|
// failures here imply the static-generator is leaking raw mustache
|
|
285
291
|
// markers to visitors again (the bug #71 closes).
|
|
286
292
|
const slot = `<body><caelo-slot name="content">_</caelo-slot></body>`;
|
|
287
293
|
|
|
288
|
-
it("AC #1 —
|
|
294
|
+
it("AC #1 — a links list iterates per entry inside composed page", () => {
|
|
289
295
|
const out = composePagePreview({
|
|
290
296
|
templateHtml: slot,
|
|
291
297
|
templateCss: "",
|
|
298
|
+
lists: LISTS,
|
|
292
299
|
blocks: [
|
|
293
300
|
{
|
|
294
301
|
blockName: "content",
|
|
@@ -297,14 +304,14 @@ describe("composePagePreview list-iteration via shared template engine (#71)", (
|
|
|
297
304
|
moduleId: "mod-nav",
|
|
298
305
|
slug: "header",
|
|
299
306
|
displayName: "Header",
|
|
300
|
-
html: '<nav>{{#nav_items}}<a href="{{
|
|
307
|
+
html: '<nav>{{#nav_items}}<a href="{{link}}">{{label}}</a>{{/nav_items}}</nav>',
|
|
301
308
|
css: "",
|
|
302
309
|
js: "",
|
|
303
|
-
fields: [{ name: "nav_items", kind: "
|
|
310
|
+
fields: [{ name: "nav_items", kind: "list", listType: "links" }],
|
|
304
311
|
contentValues: {
|
|
305
312
|
nav_items: [
|
|
306
|
-
{ label: "Docs", href: "/docs" },
|
|
307
|
-
{ label: "Blog", href: "/blog" },
|
|
313
|
+
{ id: uid(1), node: "link", values: { label: "Docs", link: { href: "/docs" } } },
|
|
314
|
+
{ id: uid(2), node: "link", values: { label: "Blog", link: { href: "/blog" } } },
|
|
308
315
|
],
|
|
309
316
|
},
|
|
310
317
|
},
|
|
@@ -315,18 +322,23 @@ describe("composePagePreview list-iteration via shared template engine (#71)", (
|
|
|
315
322
|
// Outermost element is tagged with data-caelo-module-id; assert
|
|
316
323
|
// the section iterated and that no raw markers leaked into the
|
|
317
324
|
// composed HTML.
|
|
318
|
-
expect(out.html).toContain(
|
|
319
|
-
|
|
325
|
+
expect(out.html).toContain(
|
|
326
|
+
`<a href="/docs" data-caelo-list-item="local/nav_items/${uid(1)}">Docs</a>`,
|
|
327
|
+
);
|
|
328
|
+
expect(out.html).toContain(
|
|
329
|
+
`<a href="/blog" data-caelo-list-item="local/nav_items/${uid(2)}">Blog</a>`,
|
|
330
|
+
);
|
|
320
331
|
expect(out.html).not.toMatch(/\{\{[#/]/);
|
|
321
332
|
expect(out.html).not.toMatch(/\{\{label/);
|
|
322
333
|
expect(out.html).not.toMatch(/\{\{href/);
|
|
323
334
|
expect(out.html).toContain('data-caelo-module-id="mod-nav"');
|
|
324
335
|
});
|
|
325
336
|
|
|
326
|
-
it("AC #2 —
|
|
337
|
+
it("AC #2 — a texts list iterates per entry inside composed page", () => {
|
|
327
338
|
const out = composePagePreview({
|
|
328
339
|
templateHtml: slot,
|
|
329
340
|
templateCss: "",
|
|
341
|
+
lists: LISTS,
|
|
330
342
|
blocks: [
|
|
331
343
|
{
|
|
332
344
|
blockName: "content",
|
|
@@ -335,17 +347,25 @@ describe("composePagePreview list-iteration via shared template engine (#71)", (
|
|
|
335
347
|
moduleId: "mod-tags",
|
|
336
348
|
slug: "tags",
|
|
337
349
|
displayName: "Tags",
|
|
338
|
-
html: "<ul>{{#tags}}<li>{{
|
|
350
|
+
html: "<ul>{{#tags}}<li>{{text}}</li>{{/tags}}</ul>",
|
|
339
351
|
css: "",
|
|
340
352
|
js: "",
|
|
341
|
-
fields: [{ name: "tags", kind: "
|
|
342
|
-
contentValues: {
|
|
353
|
+
fields: [{ name: "tags", kind: "list", listType: "texts" }],
|
|
354
|
+
contentValues: {
|
|
355
|
+
tags: ["a", "b", "c"].map((text, i) => ({
|
|
356
|
+
id: uid(i + 1),
|
|
357
|
+
node: "text",
|
|
358
|
+
values: { text },
|
|
359
|
+
})),
|
|
360
|
+
},
|
|
343
361
|
},
|
|
344
362
|
],
|
|
345
363
|
},
|
|
346
364
|
],
|
|
347
365
|
});
|
|
348
|
-
expect(out.html
|
|
366
|
+
expect(out.html.replace(/ data-caelo-list-item="[^"]*"/g, "")).toContain(
|
|
367
|
+
"<li>a</li><li>b</li><li>c</li>",
|
|
368
|
+
);
|
|
349
369
|
expect(out.html).not.toMatch(/\{\{[#/]/);
|
|
350
370
|
expect(out.html).toContain('data-caelo-module-id="mod-tags"');
|
|
351
371
|
});
|
|
@@ -525,11 +545,25 @@ describe("composePageWithLayout", () => {
|
|
|
525
545
|
});
|
|
526
546
|
|
|
527
547
|
it("renders a responsive nav with once-per-page functional assets (issue #160)", () => {
|
|
548
|
+
// The module owns the markup (epic #1107 — no built-in menu renderer);
|
|
549
|
+
// the interaction kit owns the behaviour.
|
|
528
550
|
const navModule = {
|
|
529
551
|
...blankModule,
|
|
530
552
|
moduleId: "33333333-3333-4333-8333-333333333303",
|
|
531
|
-
slug: "nav
|
|
532
|
-
html:
|
|
553
|
+
slug: "header-nav",
|
|
554
|
+
html:
|
|
555
|
+
'<nav class="caelo-nav-menu" data-caelo-ui="mobile-nav" data-nav-open="false">' +
|
|
556
|
+
'<button type="button" class="caelo-nav-toggle" aria-expanded="false" aria-label="Menu"></button>' +
|
|
557
|
+
'<ul>{{#menu}}<li><a href="{{link}}">{{label}}</a></li>{{/menu}}</ul></nav>',
|
|
558
|
+
fields: [
|
|
559
|
+
{
|
|
560
|
+
name: "menu",
|
|
561
|
+
kind: "list" as const,
|
|
562
|
+
listType: "menu",
|
|
563
|
+
bind: "shared" as const,
|
|
564
|
+
list: `caelo:list/${LIST.header}`,
|
|
565
|
+
},
|
|
566
|
+
],
|
|
533
567
|
};
|
|
534
568
|
const out = composePageWithLayout({
|
|
535
569
|
templateHtml,
|
|
@@ -539,10 +573,12 @@ describe("composePageWithLayout", () => {
|
|
|
539
573
|
layoutCss: "",
|
|
540
574
|
layoutBlocks: [],
|
|
541
575
|
layoutSlug: "test",
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
},
|
|
576
|
+
lists: LISTS,
|
|
577
|
+
linkResolver: fixtureLinkIndex().forPage(null),
|
|
545
578
|
});
|
|
579
|
+
expect(out.html).toContain(
|
|
580
|
+
`<li data-caelo-list-item="${LIST.header}/${ITEM.about}"><a href="/about/">About</a></li>`,
|
|
581
|
+
);
|
|
546
582
|
expect(out.html).toContain('class="caelo-nav-toggle"');
|
|
547
583
|
expect(out.html).toContain('aria-expanded="false"');
|
|
548
584
|
expect(out.html).toContain('data-nav-open="false"');
|