@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
package/src/preview-compose.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { BASE_TECHNICAL_CSS } from "./base-css.js";
|
|
|
29
29
|
import type { ModuleFieldKind } from "./content.js";
|
|
30
30
|
import { findInteractionIds, interactionAssets } from "./interactions.js";
|
|
31
31
|
import { type LinkResolver, resolveLinkRefsInHtml } from "./link-refs.js";
|
|
32
|
+
import type { ListPageContext } from "./lists/resolve.js";
|
|
32
33
|
import { tagModuleId } from "./module-id-tag.js";
|
|
33
34
|
import {
|
|
34
35
|
type NestedModuleResource,
|
|
@@ -60,8 +61,8 @@ export interface ComposeModule {
|
|
|
60
61
|
readonly js: string;
|
|
61
62
|
/**
|
|
62
63
|
* v0.4.0 module-field schema, extended in #71 to carry `kind` so
|
|
63
|
-
* the shared template engine can dispatch
|
|
64
|
-
* module
|
|
64
|
+
* the shared template engine can dispatch list / module-list /
|
|
65
|
+
* module sections (a `list` field carries its binding too). `kind` is optional for back-compat
|
|
65
66
|
* with callers that haven't been updated; the engine treats absent
|
|
66
67
|
* kinds as primitives (the legacy compose-path behaviour).
|
|
67
68
|
*
|
|
@@ -76,7 +77,7 @@ export interface ComposeModule {
|
|
|
76
77
|
* here via the `contentValues` field below. Nested-module refs
|
|
77
78
|
* render through `ComposeInput.nestedModules`.
|
|
78
79
|
*/
|
|
79
|
-
readonly fields?: readonly
|
|
80
|
+
readonly fields?: readonly ComposeField[];
|
|
80
81
|
/**
|
|
81
82
|
* PR #61 follow-up — per-placement content values from the bound
|
|
82
83
|
* `content_instances.values` jsonb. When present, the composer
|
|
@@ -87,20 +88,23 @@ export interface ComposeModule {
|
|
|
87
88
|
* visitors — the bug e2e-livedit's second Stage caught.
|
|
88
89
|
*/
|
|
89
90
|
readonly contentValues?: Readonly<Record<string, unknown>>;
|
|
91
|
+
/**
|
|
92
|
+
* The stored content instance `contentValues` come from — the address
|
|
93
|
+
* of the module's local list entries (overlays, `data-caelo-list-item`).
|
|
94
|
+
* Absent for site chrome rendered from field defaults and for content
|
|
95
|
+
* variants, which are no stored instance.
|
|
96
|
+
*/
|
|
97
|
+
readonly contentInstanceId?: string;
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/** A module field as the composer passes it to the engine; `kind` absent = primitive. */
|
|
101
|
+
export type ComposeField = Omit<TemplateField, "kind"> & { readonly kind?: ModuleFieldKind };
|
|
102
|
+
|
|
92
103
|
export interface ComposeBlock {
|
|
93
104
|
readonly blockName: string;
|
|
94
105
|
readonly modules: readonly ComposeModule[];
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
/** P6.7.5 — structured sets carried into the composer so nav-menu
|
|
98
|
-
* modules render from typed items and theme tokens flow into <head>. */
|
|
99
|
-
export interface ComposeStructuredSets {
|
|
100
|
-
/** Map keyed by `<kind>/<slug>` (e.g. `nav-menu/header-main`). */
|
|
101
|
-
readonly byKindSlug: Readonly<Record<string, readonly unknown[]>>;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
108
|
/**
|
|
105
109
|
* v0.11.0 — Theme context resolved by the preview op + static generator
|
|
106
110
|
* (#45 Phase 3). Carries the active theme's DTCG tokens jsonb plus the
|
|
@@ -149,7 +153,6 @@ export interface ComposeInput {
|
|
|
149
153
|
readonly templateHtml: string;
|
|
150
154
|
readonly templateCss: string;
|
|
151
155
|
readonly blocks: readonly ComposeBlock[];
|
|
152
|
-
readonly structuredSets?: ComposeStructuredSets;
|
|
153
156
|
/**
|
|
154
157
|
* v0.11.0 — active theme threaded through from the preview op + static
|
|
155
158
|
* generator. Undefined when no theme row exists OR when the caller
|
|
@@ -160,13 +163,14 @@ export interface ComposeInput {
|
|
|
160
163
|
/** issue #150 — resolved web fonts; undefined = system stacks only. */
|
|
161
164
|
readonly fonts?: ComposeFonts;
|
|
162
165
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
166
|
+
* The list view of the page being composed (`lists/resolve.ts`
|
|
167
|
+
* `createListRenderContext(data).forPage(pageId, …)`): resolves `list`
|
|
168
|
+
* fields — local, shared, query, system and plugin sources — and
|
|
169
|
+
* `{{page.*}}` in every placed, nested and chrome module alike, so a
|
|
170
|
+
* menu renders the same in the editor and on deploy, including the
|
|
171
|
+
* loud marker when its plugin source is off.
|
|
167
172
|
*/
|
|
168
|
-
readonly
|
|
169
|
-
readonly dormantDataLists?: Readonly<Record<string, string>>;
|
|
173
|
+
readonly lists?: ListPageContext;
|
|
170
174
|
/**
|
|
171
175
|
* #450 — modules withheld by a plugin, keyed by module id. Resolved
|
|
172
176
|
* by the host before composing; absent means the module renders
|
|
@@ -186,7 +190,7 @@ export interface ComposeInput {
|
|
|
186
190
|
/**
|
|
187
191
|
* Epic #724 — resolves internal references (`caelo:page/<id>`,
|
|
188
192
|
* `caelo:media/<id>`) for THIS page: in every placed and nested
|
|
189
|
-
* module and in
|
|
193
|
+
* module and in list entries. A broken reference becomes a
|
|
190
194
|
* `link-target-missing:<kind>/<id>` module failure; without a resolver
|
|
191
195
|
* every reference fails with `link-resolver-unavailable` (loud — a
|
|
192
196
|
* deploy refuses it).
|
|
@@ -291,25 +295,11 @@ export function composePagePreview(input: ComposeInput): ComposeOutput {
|
|
|
291
295
|
// P6.7 — tag every module's outermost element with
|
|
292
296
|
// `data-caelo-module-id="<uuid>"` so the live-edit overlay's iframe
|
|
293
297
|
// hover affordances can identify the clicked module.
|
|
294
|
-
//
|
|
295
|
-
// P6.7.5 — modules whose slug matches a `nav-menu/<slug>` set get
|
|
296
|
-
// their HTML replaced by a fresh render of the menu items. That's
|
|
297
|
-
// what makes a slug change update every menu without touching
|
|
298
|
-
// module HTML.
|
|
299
298
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
baseHtml = nav.html;
|
|
305
|
-
recordFailures(moduleFailures, block.blockName, m, nav.failures);
|
|
306
|
-
} else {
|
|
307
|
-
const rendered = renderModuleFields(m, input);
|
|
308
|
-
baseHtml = rendered.html;
|
|
309
|
-
recordFailures(moduleFailures, block.blockName, m, rendered.failures);
|
|
310
|
-
nestedAssetIds.push(...rendered.nestedModuleIds);
|
|
311
|
-
}
|
|
312
|
-
return tagModuleId(baseHtml, m.moduleId);
|
|
299
|
+
const rendered = renderModuleFields(m, input);
|
|
300
|
+
recordFailures(moduleFailures, block.blockName, m, rendered.failures);
|
|
301
|
+
nestedAssetIds.push(...rendered.nestedModuleIds);
|
|
302
|
+
return tagModuleId(rendered.html, m.moduleId);
|
|
313
303
|
});
|
|
314
304
|
const html = renderedModuleHtml.join("\n");
|
|
315
305
|
contentByName.set(block.blockName, html);
|
|
@@ -388,33 +378,6 @@ export function composePagePreview(input: ComposeInput): ComposeOutput {
|
|
|
388
378
|
};
|
|
389
379
|
}
|
|
390
380
|
|
|
391
|
-
/**
|
|
392
|
-
* P6.7.5 — return the items for a `nav-menu/<slug>` set when a module's
|
|
393
|
-
* slug starts with `nav-menu-`. Returns null when the module is not a
|
|
394
|
-
* nav menu (so the composer falls back to its stored HTML).
|
|
395
|
-
*
|
|
396
|
-
* Convention: a module slug `nav-menu-header-main` resolves to
|
|
397
|
-
* structuredSets[`nav-menu/header-main`].
|
|
398
|
-
*/
|
|
399
|
-
function lookupNavMenuItems(
|
|
400
|
-
moduleSlug: string,
|
|
401
|
-
sets: ComposeStructuredSets | undefined,
|
|
402
|
-
): readonly unknown[] | null {
|
|
403
|
-
if (!sets) return null;
|
|
404
|
-
const prefix = "nav-menu-";
|
|
405
|
-
if (!moduleSlug.startsWith(prefix)) return null;
|
|
406
|
-
const setSlug = moduleSlug.slice(prefix.length);
|
|
407
|
-
const items = sets.byKindSlug[`nav-menu/${setSlug}`];
|
|
408
|
-
return items ?? null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
interface NavMenuItem {
|
|
412
|
-
label: string;
|
|
413
|
-
href: string;
|
|
414
|
-
target?: "_self" | "_blank";
|
|
415
|
-
children?: NavMenuItem[];
|
|
416
|
-
}
|
|
417
|
-
|
|
418
381
|
function escapeAttr(s: string): string {
|
|
419
382
|
return s
|
|
420
383
|
.replace(/&/g, "&")
|
|
@@ -422,49 +385,6 @@ function escapeAttr(s: string): string {
|
|
|
422
385
|
.replace(/</g, "<")
|
|
423
386
|
.replace(/>/g, ">");
|
|
424
387
|
}
|
|
425
|
-
function escapeText(s: string): string {
|
|
426
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Render a nav-menu's typed items into HTML. Recursively handles
|
|
431
|
-
* children for submenus. Plain `<nav><ul><li>` so site CSS can theme
|
|
432
|
-
* it via the `caelo-nav-menu` class.
|
|
433
|
-
*
|
|
434
|
-
* Epic #724 — item hrefs may be internal references; they resolve
|
|
435
|
-
* through `resolver` like module content, and a broken one is returned
|
|
436
|
-
* as a failure attributed to the nav-menu module.
|
|
437
|
-
*/
|
|
438
|
-
function renderNavMenuHtml(
|
|
439
|
-
items: readonly unknown[],
|
|
440
|
-
resolver: LinkResolver | undefined,
|
|
441
|
-
): { html: string; failures: NestedRenderFailure[] } {
|
|
442
|
-
const missing: string[] = [];
|
|
443
|
-
const renderNavItem = (item: NavMenuItem): string => {
|
|
444
|
-
const target = item.target === "_blank" ? ' target="_blank" rel="noopener"' : "";
|
|
445
|
-
const inner =
|
|
446
|
-
item.children && item.children.length > 0
|
|
447
|
-
? `<ul>${item.children.map(renderNavItem).join("")}</ul>`
|
|
448
|
-
: "";
|
|
449
|
-
const href = resolveLinkRefsInHtml(item.href, resolver, missing);
|
|
450
|
-
return `<li><a href="${escapeAttr(href)}"${target}>${escapeText(item.label)}</a>${inner}</li>`;
|
|
451
|
-
};
|
|
452
|
-
const safeItems = items.filter((it): it is NavMenuItem => {
|
|
453
|
-
if (!it || typeof it !== "object") return false;
|
|
454
|
-
const o = it as { label?: unknown; href?: unknown };
|
|
455
|
-
return typeof o.label === "string" && typeof o.href === "string";
|
|
456
|
-
});
|
|
457
|
-
// issue #160 — mobile-ready markup: toggle button (three functional
|
|
458
|
-
// bars via currentColor) + collapsible list. `data-caelo-ui` pulls the
|
|
459
|
-
// functional CSS/JS from ./interactions.ts in once per page.
|
|
460
|
-
const toggle =
|
|
461
|
-
'<button type="button" class="caelo-nav-toggle" aria-expanded="false" aria-label="Menu">' +
|
|
462
|
-
'<span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span>' +
|
|
463
|
-
"</button>";
|
|
464
|
-
const html = `<nav class="caelo-nav-menu" data-caelo-ui="mobile-nav" data-nav-open="false">${toggle}<ul>${safeItems.map(renderNavItem).join("")}</ul></nav>`;
|
|
465
|
-
return { html, failures: missing.map((reason) => ({ field: "", reason })) };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
388
|
/**
|
|
469
389
|
* v0.11.0 — Render the active theme's DTCG tokens as `:root { … }` (+
|
|
470
390
|
* optional `:root.dark { … }`) CSS. Returns null when no active theme
|
|
@@ -516,19 +436,16 @@ function renderModuleFields(
|
|
|
516
436
|
m: ComposeModule,
|
|
517
437
|
input: {
|
|
518
438
|
readonly theme?: ComposeTheme;
|
|
519
|
-
readonly
|
|
520
|
-
readonly dormantDataLists?: Readonly<Record<string, string>>;
|
|
439
|
+
readonly lists?: ListPageContext;
|
|
521
440
|
readonly nestedModules?: NestedRenderResolver;
|
|
522
441
|
readonly linkResolver?: LinkResolver;
|
|
523
442
|
},
|
|
524
443
|
): RenderedModuleFields {
|
|
525
444
|
// The substitution engine (renderTemplate) already unwraps CDATA
|
|
526
445
|
// guards; cover the no-op early-return path so a chrome module with no
|
|
527
|
-
// fields/values/theme is cleaned too.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
Object.keys(input.dormantDataLists ?? {}).length > 0;
|
|
531
|
-
if (!m.fields && !m.contentValues && !input.theme && !hasLists) {
|
|
446
|
+
// fields/values/theme is cleaned too. A list context means the HTML
|
|
447
|
+
// may iterate a plugin source or read `{{page.*}}` without any field.
|
|
448
|
+
if (!m.fields && !m.contentValues && !input.theme && !input.lists) {
|
|
532
449
|
// Still resolve references: pre-rendered (editor preview) and
|
|
533
450
|
// field-less chrome modules carry links in their raw HTML too.
|
|
534
451
|
const missing: string[] = [];
|
|
@@ -539,11 +456,7 @@ function renderModuleFields(
|
|
|
539
456
|
nestedModuleIds: [],
|
|
540
457
|
};
|
|
541
458
|
}
|
|
542
|
-
const fields: TemplateField[] = (m.fields ?? []).map((f) => ({
|
|
543
|
-
name: f.name,
|
|
544
|
-
kind: f.kind ?? "text",
|
|
545
|
-
default: f.default,
|
|
546
|
-
}));
|
|
459
|
+
const fields: TemplateField[] = (m.fields ?? []).map((f) => ({ ...f, kind: f.kind ?? "text" }));
|
|
547
460
|
// v0.11.1 (issue #76) — thread the active theme's asset URLs so
|
|
548
461
|
// module HTML carrying `{{theme_logo_url}}` etc. resolves. Unbound
|
|
549
462
|
// slots emit loud-raw + `theme-asset-unbound:<slot>` markers.
|
|
@@ -565,9 +478,9 @@ function renderModuleFields(
|
|
|
565
478
|
input.nestedModules,
|
|
566
479
|
{
|
|
567
480
|
themeAssets,
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
linkResolver: input.linkResolver,
|
|
481
|
+
...(input.lists ? { lists: input.lists } : {}),
|
|
482
|
+
...(m.contentInstanceId ? { contentInstanceId: m.contentInstanceId } : {}),
|
|
483
|
+
...(input.linkResolver ? { linkResolver: input.linkResolver } : {}),
|
|
571
484
|
},
|
|
572
485
|
);
|
|
573
486
|
return { html: r.html, failures: r.failures, nestedModuleIds: [...r.touchedModuleIds] };
|
|
@@ -576,8 +489,8 @@ function renderModuleFields(
|
|
|
576
489
|
html: m.html,
|
|
577
490
|
fields,
|
|
578
491
|
contentValues: m.contentValues,
|
|
579
|
-
|
|
580
|
-
|
|
492
|
+
...(input.lists ? { lists: input.lists } : {}),
|
|
493
|
+
listHolder: { contentInstanceId: m.contentInstanceId ?? null },
|
|
581
494
|
themeAssets,
|
|
582
495
|
linkResolver: input.linkResolver,
|
|
583
496
|
});
|
|
@@ -796,35 +709,27 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
|
|
|
796
709
|
const deferral = input.deferredModules?.[m.moduleId];
|
|
797
710
|
// Nested modules rendered inside THIS placement when it is withheld.
|
|
798
711
|
const gatedNestedIds = new Set<string>();
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
// Inside a withheld parent the nested module is withheld too:
|
|
821
|
-
// its CSS/JS go into the parent's <template>, never the page-wide
|
|
822
|
-
// bundles, or its `fetch()` / `url(…)` would run before consent.
|
|
823
|
-
if (deferral) {
|
|
824
|
-
if (id !== m.moduleId) gatedNestedIds.add(id);
|
|
825
|
-
} else {
|
|
826
|
-
nestedAssetIds.push(id);
|
|
827
|
-
}
|
|
712
|
+
const rendered = renderModuleFields(m, input);
|
|
713
|
+
const baseHtml = rendered.html;
|
|
714
|
+
recordFailures(moduleFailures, blockName, m, rendered.failures);
|
|
715
|
+
for (const id of rendered.nestedModuleIds) {
|
|
716
|
+
const nestedDeferral = input.deferredModules?.[id];
|
|
717
|
+
if (nestedDeferral) {
|
|
718
|
+
const nestedSlug = input.nestedModules?.getModule(id)?.slug ?? id;
|
|
719
|
+
throw new ComposeError(
|
|
720
|
+
"nested-module-deferred",
|
|
721
|
+
`module "${nestedSlug}" is withheld by plugin "${nestedDeferral.pluginSlug}" (${nestedDeferral.reason}) but sits inside module "${m.slug}" in block "${blockName}", where it cannot be gated — ` +
|
|
722
|
+
"place it on the page directly instead of inside another module's field",
|
|
723
|
+
input.layoutSlug,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
// Inside a withheld parent the nested module is withheld too:
|
|
727
|
+
// its CSS/JS go into the parent's <template>, never the page-wide
|
|
728
|
+
// bundles, or its `fetch()` / `url(…)` would run before consent.
|
|
729
|
+
if (deferral) {
|
|
730
|
+
if (id !== m.moduleId) gatedNestedIds.add(id);
|
|
731
|
+
} else {
|
|
732
|
+
nestedAssetIds.push(id);
|
|
828
733
|
}
|
|
829
734
|
}
|
|
830
735
|
const tagged = tagModuleId(baseHtml, m.moduleId);
|
|
@@ -905,8 +810,8 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
|
|
|
905
810
|
let html = replaced.html;
|
|
906
811
|
|
|
907
812
|
// issue #160 — every vetted interaction the page's markup asks for
|
|
908
|
-
// (`data-caelo-ui="…"`, from modules, the
|
|
909
|
-
//
|
|
813
|
+
// (`data-caelo-ui="…"`, from modules, the layout or the template,
|
|
814
|
+
// withheld modules included) ships once, after module
|
|
910
815
|
// CSS/JS so its functional rules win over a module's display rules.
|
|
911
816
|
const interactions = interactionAssets(new Set(findInteractionIds(html).known));
|
|
912
817
|
cssParts.push(...interactions.css);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { createTemplateToolInput } from "./ai-tools.js";
|
|
5
|
+
import {
|
|
6
|
+
decodeHtmlEscapedSource,
|
|
7
|
+
decodeJsonEncodedSource,
|
|
8
|
+
decodeSourceTextTransport,
|
|
9
|
+
} from "./source-text-transport.js";
|
|
10
|
+
|
|
11
|
+
const markup = '<main class="simple-page-main"><caelo-slot name="content"></caelo-slot></main>';
|
|
12
|
+
|
|
13
|
+
describe("decodeHtmlEscapedSource", () => {
|
|
14
|
+
it("decodes markup escaped as a whole (e2e-livedit run 40344374447)", () => {
|
|
15
|
+
const sent =
|
|
16
|
+
'<main class="simple-page-main"><caelo-slot name="content"></caelo-slot></main>';
|
|
17
|
+
expect(decodeHtmlEscapedSource(sent)).toBe(markup);
|
|
18
|
+
expect(
|
|
19
|
+
decodeHtmlEscapedSource("<p class="x">Tom &amp; Jerry</p>"),
|
|
20
|
+
).toBe('<p class="x">Tom & Jerry</p>');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("leaves real markup and text that only mentions an entity untouched", () => {
|
|
24
|
+
const withEntity = "<p>Use <div> for blocks</p>";
|
|
25
|
+
expect(decodeHtmlEscapedSource(withEntity)).toBe(withEntity);
|
|
26
|
+
expect(decodeHtmlEscapedSource("a < b")).toBe("a < b");
|
|
27
|
+
expect(decodeHtmlEscapedSource(".a > .b { color: red }")).toBe(".a > .b { color: red }");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("decodeJsonEncodedSource", () => {
|
|
32
|
+
it("decodes a whole-value JSON string literal with escapes", () => {
|
|
33
|
+
expect(decodeJsonEncodedSource(JSON.stringify(markup))).toBe(markup);
|
|
34
|
+
expect(decodeJsonEncodedSource('".a {\\n color: red;\\n}"')).toBe(".a {\n color: red;\n}");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("leaves a literal without escapes and non-literals untouched", () => {
|
|
38
|
+
expect(decodeJsonEncodedSource('"use strict"')).toBe('"use strict"');
|
|
39
|
+
expect(decodeJsonEncodedSource(markup)).toBe(markup);
|
|
40
|
+
expect(decodeJsonEncodedSource('"broken \\" literal')).toBe('"broken \\" literal');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("decodeSourceTextTransport", () => {
|
|
45
|
+
it("unwraps CDATA, then a JSON literal, then HTML escaping", () => {
|
|
46
|
+
expect(decodeSourceTextTransport(`<![CDATA[${markup}]]>`)).toBe(markup);
|
|
47
|
+
expect(
|
|
48
|
+
decodeSourceTextTransport(
|
|
49
|
+
JSON.stringify(markup.replaceAll("<", "<").replaceAll(">", ">")),
|
|
50
|
+
),
|
|
51
|
+
).toBe(markup);
|
|
52
|
+
expect(decodeSourceTextTransport(42)).toBe(42);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is what create_template's html argument goes through", () => {
|
|
56
|
+
const r = createTemplateToolInput.parse({
|
|
57
|
+
slug: "page-template",
|
|
58
|
+
displayName: "Simple Page",
|
|
59
|
+
html: markup.replaceAll("<", "<").replaceAll(">", ">"),
|
|
60
|
+
});
|
|
61
|
+
expect(r.html).toBe(markup);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decoding of whole-value transport encodings around AI tool arguments that
|
|
5
|
+
* carry source text (module / template / layout html, css, js).
|
|
6
|
+
*
|
|
7
|
+
* The model sometimes encodes a long markup string as a whole before it
|
|
8
|
+
* sends it: as a CDATA section (`unwrapCdataEnvelope`), as a JSON string
|
|
9
|
+
* literal (`"<footer class=\"x\">…"`), or HTML-escaped (`<main>…`).
|
|
10
|
+
* Each is an encoding of a value the caller did supply, never content:
|
|
11
|
+
* a JSON string literal is not valid markup, CSS or JS as a whole, and
|
|
12
|
+
* markup without a single `<` but with `<tag` has no element at all.
|
|
13
|
+
* Refusing them cost a tool round-trip each time (e2e-livedit runs of
|
|
14
|
+
* PR #1121 and #1143); describing them in every tool cost tokens on every
|
|
15
|
+
* call and still did not stop them. So the boundary decodes them, once,
|
|
16
|
+
* here. Anything not clearly a whole-value encoding is left untouched and
|
|
17
|
+
* the op validator still refuses it (shared `content.ts`), which also
|
|
18
|
+
* keeps human and import writes strict. This is input normalisation, not
|
|
19
|
+
* a fallback for missing data (CLAUDE.md §2).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { unwrapCdataEnvelope } from "./strip-cdata.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The body of a whole-value JSON string literal, or the input unchanged.
|
|
26
|
+
* Only decoded when the literal carries an escape (`\"`, `\n`, `\t`, `\\`,
|
|
27
|
+
* `\/`, `\u…`): a bare `"use strict"` stays as it is.
|
|
28
|
+
*/
|
|
29
|
+
export function decodeJsonEncodedSource(value: unknown): unknown {
|
|
30
|
+
if (typeof value !== "string") return value;
|
|
31
|
+
const t = value.trim();
|
|
32
|
+
if (t.length < 2 || !t.startsWith('"') || !t.endsWith('"')) return value;
|
|
33
|
+
if (!/\\["ntr\\/u]/.test(t)) return value;
|
|
34
|
+
try {
|
|
35
|
+
const decoded: unknown = JSON.parse(t);
|
|
36
|
+
return typeof decoded === "string" ? decoded : value;
|
|
37
|
+
} catch {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ENTITIES: Readonly<Record<string, string>> = {
|
|
43
|
+
"<": "<",
|
|
44
|
+
">": ">",
|
|
45
|
+
""": '"',
|
|
46
|
+
""": '"',
|
|
47
|
+
"'": "'",
|
|
48
|
+
"'": "'",
|
|
49
|
+
"'": "'",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Markup sent HTML-escaped as a whole, decoded once; the input unchanged
|
|
54
|
+
* otherwise. "As a whole" = no raw `<` anywhere and at least one escaped
|
|
55
|
+
* tag (`<div`, `</div`) — the same test the op's refusal uses.
|
|
56
|
+
* `&` is decoded last, so an entity the author meant (`&nbsp;`
|
|
57
|
+
* sent for ` `) comes out as the author wrote it.
|
|
58
|
+
*/
|
|
59
|
+
export function decodeHtmlEscapedSource(value: unknown): unknown {
|
|
60
|
+
if (typeof value !== "string") return value;
|
|
61
|
+
if (value.includes("<") || !/<\/?[a-zA-Z]/.test(value)) return value;
|
|
62
|
+
return value
|
|
63
|
+
.replace(/<|>|"|"|'|'|'/g, (e) => ENTITIES[e] ?? e)
|
|
64
|
+
.replace(/&/g, "&");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Every whole-value transport encoding, outermost first. */
|
|
68
|
+
export function decodeSourceTextTransport(value: unknown): unknown {
|
|
69
|
+
return decodeHtmlEscapedSource(decodeJsonEncodedSource(unwrapCdataEnvelope(value)));
|
|
70
|
+
}
|