@caelo-cms/shared 0.10.22 → 0.10.24
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 +289 -273
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +342 -323
- package/dist/ai-tools.js.map +1 -1
- package/dist/auth-forms.d.ts.map +1 -1
- package/dist/auth-forms.js +4 -1
- package/dist/auth-forms.js.map +1 -1
- package/dist/base-css.d.ts +35 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +40 -0
- package/dist/base-css.js.map +1 -0
- package/dist/build-page.d.ts +330 -0
- package/dist/build-page.d.ts.map +1 -0
- package/dist/build-page.js +282 -0
- package/dist/build-page.js.map +1 -0
- package/dist/content.d.ts +322 -9
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +354 -11
- package/dist/content.js.map +1 -1
- package/dist/css-gradient-scan.d.ts +14 -0
- package/dist/css-gradient-scan.d.ts.map +1 -0
- package/dist/css-gradient-scan.js +81 -0
- package/dist/css-gradient-scan.js.map +1 -0
- package/dist/css-var-scan.d.ts +56 -0
- package/dist/css-var-scan.d.ts.map +1 -0
- package/dist/css-var-scan.js +97 -0
- package/dist/css-var-scan.js.map +1 -0
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/design-manifest.d.ts +36 -0
- package/dist/design-manifest.d.ts.map +1 -0
- package/dist/design-manifest.js +90 -0
- package/dist/design-manifest.js.map +1 -0
- package/dist/fonts.d.ts +89 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +241 -0
- package/dist/fonts.js.map +1 -0
- package/dist/genesis-inventory.d.ts +32 -0
- package/dist/genesis-inventory.d.ts.map +1 -0
- package/dist/genesis-inventory.js +186 -0
- package/dist/genesis-inventory.js.map +1 -0
- package/dist/genesis.d.ts +102 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +145 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +29 -36
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +53 -128
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.d.ts +23 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +44 -0
- package/dist/interactions.js.map +1 -0
- package/dist/media.d.ts +101 -16
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +126 -15
- package/dist/media.js.map +1 -1
- package/dist/page-log.d.ts +94 -0
- package/dist/page-log.d.ts.map +1 -0
- package/dist/page-log.js +111 -0
- package/dist/page-log.js.map +1 -0
- package/dist/preview-compose.d.ts +85 -12
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +157 -67
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +40 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +34 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/responsive-images.d.ts +64 -0
- package/dist/responsive-images.d.ts.map +1 -0
- package/dist/responsive-images.js +98 -0
- package/dist/responsive-images.js.map +1 -0
- package/dist/safe-keys.d.ts +9 -0
- package/dist/safe-keys.d.ts.map +1 -0
- package/dist/safe-keys.js +20 -0
- package/dist/safe-keys.js.map +1 -0
- package/dist/seo.d.ts +11 -16
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +7 -23
- package/dist/seo.js.map +1 -1
- package/dist/skills.d.ts +14 -68
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +19 -113
- package/dist/skills.js.map +1 -1
- package/dist/strip-cdata.d.ts +7 -0
- package/dist/strip-cdata.d.ts.map +1 -0
- package/dist/strip-cdata.js +48 -0
- package/dist/strip-cdata.js.map +1 -0
- package/dist/structured-sets.d.ts +6 -52
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +6 -68
- package/dist/structured-sets.js.map +1 -1
- package/dist/subagents.d.ts +105 -3
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +224 -41
- package/dist/subagents.js.map +1 -1
- package/dist/template-engine.d.ts +85 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +403 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/theme-importers/auto-detect.d.ts +26 -0
- package/dist/theme-importers/auto-detect.d.ts.map +1 -0
- package/dist/theme-importers/auto-detect.js +42 -0
- package/dist/theme-importers/auto-detect.js.map +1 -0
- package/dist/theme-importers/css-comments.d.ts +12 -0
- package/dist/theme-importers/css-comments.d.ts.map +1 -0
- package/dist/theme-importers/css-comments.js +15 -0
- package/dist/theme-importers/css-comments.js.map +1 -0
- package/dist/theme-importers/dtcg.d.ts +46 -0
- package/dist/theme-importers/dtcg.d.ts.map +1 -0
- package/dist/theme-importers/dtcg.js +111 -0
- package/dist/theme-importers/dtcg.js.map +1 -0
- package/dist/theme-importers/loose.d.ts +3 -0
- package/dist/theme-importers/loose.d.ts.map +1 -0
- package/dist/theme-importers/loose.js +76 -0
- package/dist/theme-importers/loose.js.map +1 -0
- package/dist/theme-importers/shadcn.d.ts +24 -0
- package/dist/theme-importers/shadcn.d.ts.map +1 -0
- package/dist/theme-importers/shadcn.js +135 -0
- package/dist/theme-importers/shadcn.js.map +1 -0
- package/dist/theme-importers/style-dictionary.d.ts +17 -0
- package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
- package/dist/theme-importers/style-dictionary.js +125 -0
- package/dist/theme-importers/style-dictionary.js.map +1 -0
- package/dist/theme-importers/tailwind.d.ts +3 -0
- package/dist/theme-importers/tailwind.d.ts.map +1 -0
- package/dist/theme-importers/tailwind.js +218 -0
- package/dist/theme-importers/tailwind.js.map +1 -0
- package/dist/theme-literal-binding.d.ts +37 -0
- package/dist/theme-literal-binding.d.ts.map +1 -0
- package/dist/theme-literal-binding.js +138 -0
- package/dist/theme-literal-binding.js.map +1 -0
- package/dist/theme-normalize.d.ts +31 -0
- package/dist/theme-normalize.d.ts.map +1 -0
- package/dist/theme-normalize.js +587 -0
- package/dist/theme-normalize.js.map +1 -0
- package/dist/theme-ramp.d.ts +55 -0
- package/dist/theme-ramp.d.ts.map +1 -0
- package/dist/theme-ramp.js +149 -0
- package/dist/theme-ramp.js.map +1 -0
- package/dist/theme-render.d.ts +105 -0
- package/dist/theme-render.d.ts.map +1 -0
- package/dist/theme-render.js +441 -0
- package/dist/theme-render.js.map +1 -0
- package/dist/themes-errors.d.ts +109 -0
- package/dist/themes-errors.d.ts.map +1 -0
- package/dist/themes-errors.js +170 -0
- package/dist/themes-errors.js.map +1 -0
- package/dist/themes.d.ts +343 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +697 -0
- package/dist/themes.js.map +1 -0
- package/dist/version.d.ts +7 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -2
- package/src/__tests__/redos-hardening.test.ts +160 -0
- package/src/ai-tools-add-module-modes.test.ts +106 -0
- package/src/ai-tools-position.test.ts +134 -0
- package/src/ai-tools.test.ts +81 -0
- package/src/ai-tools.ts +1105 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +42 -0
- package/src/build-page.test.ts +228 -0
- package/src/build-page.ts +319 -0
- package/src/cap-failures.ts +67 -0
- package/src/content.test.ts +170 -0
- package/src/content.ts +620 -0
- package/src/context.ts +43 -0
- package/src/css-gradient-scan.ts +88 -0
- package/src/css-var-scan.test.ts +96 -0
- package/src/css-var-scan.ts +144 -0
- package/src/derive-module-type.test.ts +80 -0
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/design-manifest.ts +93 -0
- package/src/fonts.test.ts +157 -0
- package/src/fonts.ts +296 -0
- package/src/genesis-inventory.test.ts +86 -0
- package/src/genesis-inventory.ts +215 -0
- package/src/genesis-sanitize.test.ts +35 -0
- package/src/genesis.ts +158 -0
- package/src/i18n.test.ts +58 -0
- package/src/i18n.ts +91 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +59 -0
- package/src/interactions.ts +48 -0
- package/src/logger.ts +147 -0
- package/src/media.test.ts +160 -0
- package/src/media.ts +355 -0
- package/src/page-log.test.ts +163 -0
- package/src/page-log.ts +124 -0
- package/src/preview-compose.test.ts +637 -0
- package/src/preview-compose.ts +656 -0
- package/src/preview-scanner.test.ts +96 -0
- package/src/preview-scanner.ts +214 -0
- package/src/proposal-status.test.ts +69 -0
- package/src/proposal-status.ts +40 -0
- package/src/responsive-images.test.ts +104 -0
- package/src/responsive-images.ts +151 -0
- package/src/result.ts +29 -0
- package/src/safe-keys.ts +21 -0
- package/src/seo.test.ts +194 -0
- package/src/seo.ts +233 -0
- package/src/skills.ts +48 -0
- package/src/snapshots.test.ts +80 -0
- package/src/snapshots.ts +81 -0
- package/src/strip-cdata.test.ts +41 -0
- package/src/strip-cdata.ts +50 -0
- package/src/structured-sets.ts +114 -0
- package/src/subagents.test.ts +262 -0
- package/src/subagents.ts +432 -0
- package/src/template-engine.test.ts +379 -0
- package/src/template-engine.ts +520 -0
- package/src/theme-gradient.test.ts +92 -0
- package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
- package/src/theme-importers/auto-detect.ts +84 -0
- package/src/theme-importers/css-comments.ts +15 -0
- package/src/theme-importers/dtcg.ts +106 -0
- package/src/theme-importers/loose.ts +76 -0
- package/src/theme-importers/shadcn.ts +133 -0
- package/src/theme-importers/style-dictionary.ts +125 -0
- package/src/theme-importers/tailwind.ts +217 -0
- package/src/theme-literal-binding.test.ts +71 -0
- package/src/theme-literal-binding.ts +159 -0
- package/src/theme-motion.test.ts +115 -0
- package/src/theme-normalize-envelope.test.ts +43 -0
- package/src/theme-normalize-gradient.test.ts +135 -0
- package/src/theme-normalize.ts +661 -0
- package/src/theme-ramp.ts +187 -0
- package/src/theme-render-sanitize.test.ts +45 -0
- package/src/theme-render.test.ts +119 -0
- package/src/theme-render.ts +487 -0
- package/src/theme-shadow.test.ts +56 -0
- package/src/themes-errors.ts +199 -0
- package/src/themes.ts +842 -0
- package/src/version.ts +66 -0
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tier-1 unit tests for the shared template engine (Plan B per #71).
|
|
5
|
+
*
|
|
6
|
+
* Each test pins one of the §8.1 cases in the implementation plan and
|
|
7
|
+
* states the exact expected output so a future regression points
|
|
8
|
+
* directly at the affected branch.
|
|
9
|
+
*
|
|
10
|
+
* Failure-marker tests (kind-mismatch, *-malformed, module-ref-malformed,
|
|
11
|
+
* field-not-declared) assert the literal `missingSlots` string the
|
|
12
|
+
* chat-runner diag pass + editor missing-content surface read today —
|
|
13
|
+
* any rename to the marker shape silently breaks those callers, so the
|
|
14
|
+
* tests intentionally match strings, not regexes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it } from "bun:test";
|
|
18
|
+
import { renderTemplate, type TemplateField } from "./template-engine.js";
|
|
19
|
+
|
|
20
|
+
describe("renderTemplate — primitive substitution", () => {
|
|
21
|
+
it("substitutes {{name}} with contentValues[name] (happy path)", () => {
|
|
22
|
+
const r = renderTemplate({
|
|
23
|
+
html: "<h1>{{title}}</h1>",
|
|
24
|
+
fields: [{ name: "title", kind: "text" }],
|
|
25
|
+
contentValues: { title: "Hello" },
|
|
26
|
+
});
|
|
27
|
+
expect(r.html).toBe("<h1>Hello</h1>");
|
|
28
|
+
expect(r.missingSlots).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("contentValues beats field.default (per-placement wins)", () => {
|
|
32
|
+
const r = renderTemplate({
|
|
33
|
+
html: "<h1>{{title}}</h1>",
|
|
34
|
+
fields: [{ name: "title", kind: "text", default: "D" }],
|
|
35
|
+
contentValues: { title: "Override" },
|
|
36
|
+
});
|
|
37
|
+
expect(r.html).toBe("<h1>Override</h1>");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to field.default when contentValues is absent", () => {
|
|
41
|
+
const r = renderTemplate({
|
|
42
|
+
html: "<h1>{{title}}</h1>",
|
|
43
|
+
fields: [{ name: "title", kind: "text", default: "D" }],
|
|
44
|
+
contentValues: {},
|
|
45
|
+
});
|
|
46
|
+
expect(r.html).toBe("<h1>D</h1>");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("leaves unknown {{name}} as raw markers (loud-raw) and tracks field-not-declared", () => {
|
|
50
|
+
const r = renderTemplate({
|
|
51
|
+
html: "<h1>{{ghost}}</h1>",
|
|
52
|
+
fields: [],
|
|
53
|
+
contentValues: {},
|
|
54
|
+
});
|
|
55
|
+
expect(r.html).toBe("<h1>{{ghost}}</h1>");
|
|
56
|
+
expect(r.missingSlots).toContain("field-not-declared:ghost");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves declared-but-empty {{name}} as raw markers without tracking missingSlots", () => {
|
|
60
|
+
// A declared field with no contentValue and no default is the
|
|
61
|
+
// operator's responsibility (still authoring), not a system-side
|
|
62
|
+
// gap callers should warn about. Loud-raw in HTML, silent in the
|
|
63
|
+
// structured channel.
|
|
64
|
+
const r = renderTemplate({
|
|
65
|
+
html: "<h1>{{title}}</h1>",
|
|
66
|
+
fields: [{ name: "title", kind: "text" }],
|
|
67
|
+
contentValues: {},
|
|
68
|
+
});
|
|
69
|
+
expect(r.html).toBe("<h1>{{title}}</h1>");
|
|
70
|
+
expect(r.missingSlots).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("case-insensitive name match — {{ctaHref}} finds contentValues.ctahref", () => {
|
|
74
|
+
const r = renderTemplate({
|
|
75
|
+
html: "<h1>{{ctaHref}}</h1>",
|
|
76
|
+
fields: [{ name: "ctahref", kind: "url" }],
|
|
77
|
+
contentValues: { ctahref: "/x" },
|
|
78
|
+
});
|
|
79
|
+
expect(r.html).toBe("<h1>/x</h1>");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does NOT HTML-escape values (modules ARE the place raw HTML lives, §3.1)", () => {
|
|
83
|
+
const r = renderTemplate({
|
|
84
|
+
html: '<a href="{{u}}">x</a>',
|
|
85
|
+
fields: [{ name: "u", kind: "url" }],
|
|
86
|
+
contentValues: { u: "/docs?a=1&b=2" },
|
|
87
|
+
});
|
|
88
|
+
expect(r.html).toBe('<a href="/docs?a=1&b=2">x</a>');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("renderTemplate — text-list section", () => {
|
|
93
|
+
const textListField: TemplateField = { name: "items", kind: "text-list" };
|
|
94
|
+
|
|
95
|
+
it("iterates with {{.}} per element", () => {
|
|
96
|
+
const r = renderTemplate({
|
|
97
|
+
html: "<ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>",
|
|
98
|
+
fields: [textListField],
|
|
99
|
+
contentValues: { items: ["a", "b", "c"] },
|
|
100
|
+
});
|
|
101
|
+
expect(r.html).toBe("<ul><li>a</li><li>b</li><li>c</li></ul>");
|
|
102
|
+
expect(r.missingSlots).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("iterates with {{item}} alias per element", () => {
|
|
106
|
+
const r = renderTemplate({
|
|
107
|
+
html: "<ul>{{#items}}<li>{{item}}</li>{{/items}}</ul>",
|
|
108
|
+
fields: [textListField],
|
|
109
|
+
contentValues: { items: ["a", "b", "c"] },
|
|
110
|
+
});
|
|
111
|
+
expect(r.html).toBe("<ul><li>a</li><li>b</li><li>c</li></ul>");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("coerces numeric + boolean elements to strings", () => {
|
|
115
|
+
const r = renderTemplate({
|
|
116
|
+
html: "<ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>",
|
|
117
|
+
fields: [textListField],
|
|
118
|
+
contentValues: { items: [1, true, "x"] },
|
|
119
|
+
});
|
|
120
|
+
expect(r.html).toBe("<ul><li>1</li><li>true</li><li>x</li></ul>");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("empty array renders as empty section, no missingSlots", () => {
|
|
124
|
+
const r = renderTemplate({
|
|
125
|
+
html: "<ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>",
|
|
126
|
+
fields: [textListField],
|
|
127
|
+
contentValues: { items: [] },
|
|
128
|
+
});
|
|
129
|
+
expect(r.html).toBe("<ul></ul>");
|
|
130
|
+
expect(r.missingSlots).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("per-element malformed object becomes loud comment + tracks text-list-malformed", () => {
|
|
134
|
+
const r = renderTemplate({
|
|
135
|
+
html: "<ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>",
|
|
136
|
+
fields: [textListField],
|
|
137
|
+
contentValues: { items: ["a", { bogus: true }, "c"] },
|
|
138
|
+
});
|
|
139
|
+
expect(r.html).toContain("<li>a</li>");
|
|
140
|
+
expect(r.html).toContain("<li>c</li>");
|
|
141
|
+
expect(r.html).toContain("<!-- caelo:missing reason=text-list-malformed items[1] -->");
|
|
142
|
+
expect(r.missingSlots).toContain("text-list-malformed:items[1]");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("renderTemplate — link-list section (AC #1)", () => {
|
|
147
|
+
const linkListField: TemplateField = { name: "nav_items", kind: "link-list" };
|
|
148
|
+
|
|
149
|
+
it("AC #1 verbatim fixture", () => {
|
|
150
|
+
const r = renderTemplate({
|
|
151
|
+
html: '<nav>{{#nav_items}}<a href="{{href}}">{{label}}</a>{{/nav_items}}</nav>',
|
|
152
|
+
fields: [linkListField],
|
|
153
|
+
contentValues: {
|
|
154
|
+
nav_items: [
|
|
155
|
+
{ label: "Docs", href: "/docs" },
|
|
156
|
+
{ label: "Blog", href: "/blog" },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
expect(r.html).toBe('<nav><a href="/docs">Docs</a><a href="/blog">Blog</a></nav>');
|
|
161
|
+
expect(r.html).not.toMatch(/\{\{[#/]/);
|
|
162
|
+
expect(r.html).not.toMatch(/\{\{label/);
|
|
163
|
+
expect(r.html).not.toMatch(/\{\{href/);
|
|
164
|
+
expect(r.missingSlots).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("per-element malformed (missing href) becomes loud comment + tracks link-list-malformed", () => {
|
|
168
|
+
const r = renderTemplate({
|
|
169
|
+
html: '<nav>{{#nav_items}}<a href="{{href}}">{{label}}</a>{{/nav_items}}</nav>',
|
|
170
|
+
fields: [linkListField],
|
|
171
|
+
contentValues: {
|
|
172
|
+
nav_items: [
|
|
173
|
+
{ label: "OK", href: "/a" },
|
|
174
|
+
{ label: "OK only" }, // missing href
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
expect(r.html).toContain('<a href="/a">OK</a>');
|
|
179
|
+
expect(r.html).toContain("<!-- caelo:missing reason=link-list-malformed nav_items[1] -->");
|
|
180
|
+
expect(r.missingSlots).toContain("link-list-malformed:nav_items[1]");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("renderTemplate — module-list section", () => {
|
|
185
|
+
const moduleListField: TemplateField = { name: "cards", kind: "module-list" };
|
|
186
|
+
|
|
187
|
+
it("renders each element from partials[<name>__<index>] in order", () => {
|
|
188
|
+
const r = renderTemplate({
|
|
189
|
+
html: "<ul>{{#cards}}<li>ignored</li>{{/cards}}</ul>",
|
|
190
|
+
fields: [moduleListField],
|
|
191
|
+
contentValues: {
|
|
192
|
+
cards: [
|
|
193
|
+
{ moduleId: "m1", contentInstanceId: "c1" },
|
|
194
|
+
{ moduleId: "m2", contentInstanceId: "c2" },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
partials: {
|
|
198
|
+
cards__0: "<article>A</article>",
|
|
199
|
+
cards__1: "<article>B</article>",
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
expect(r.html).toBe("<ul><article>A</article><article>B</article></ul>");
|
|
203
|
+
expect(r.html).not.toMatch(/\{\{[#/]/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("compose-path posture — no partials supplied → loud comment per element", () => {
|
|
207
|
+
const r = renderTemplate({
|
|
208
|
+
html: "<ul>{{#cards}}<li>ignored</li>{{/cards}}</ul>",
|
|
209
|
+
fields: [moduleListField],
|
|
210
|
+
contentValues: {
|
|
211
|
+
cards: [{ moduleId: "m1", contentInstanceId: "c1" }],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
expect(r.html).toContain(
|
|
215
|
+
"<!-- caelo:module-list cards needs recursive renderer (compose path) -->",
|
|
216
|
+
);
|
|
217
|
+
expect(r.html).not.toMatch(/\{\{[#/]/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("per-element malformed ref becomes loud comment + tracks module-list-malformed", () => {
|
|
221
|
+
const r = renderTemplate({
|
|
222
|
+
html: "<ul>{{#cards}}<li>x</li>{{/cards}}</ul>",
|
|
223
|
+
fields: [moduleListField],
|
|
224
|
+
contentValues: {
|
|
225
|
+
cards: [{ moduleId: "m1", contentInstanceId: "c1" }, "not-a-ref"],
|
|
226
|
+
},
|
|
227
|
+
partials: { cards__0: "<article>A</article>" },
|
|
228
|
+
});
|
|
229
|
+
expect(r.html).toContain("<article>A</article>");
|
|
230
|
+
expect(r.html).toContain("<!-- caelo:missing reason=module-list-malformed cards[1] -->");
|
|
231
|
+
expect(r.missingSlots).toContain("module-list-malformed:cards[1]");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("renderTemplate — single nested {{>name}}", () => {
|
|
236
|
+
const moduleField: TemplateField = { name: "hero", kind: "module" };
|
|
237
|
+
|
|
238
|
+
it("renders partials[<name>] inline", () => {
|
|
239
|
+
const r = renderTemplate({
|
|
240
|
+
html: "<aside>{{>hero}}</aside>",
|
|
241
|
+
fields: [moduleField],
|
|
242
|
+
contentValues: { hero: { moduleId: "m", contentInstanceId: "c" } },
|
|
243
|
+
partials: { hero: "<h1>X</h1>" },
|
|
244
|
+
});
|
|
245
|
+
expect(r.html).toBe("<aside><h1>X</h1></aside>");
|
|
246
|
+
expect(r.missingSlots).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("compose-path posture — no partial supplied → loud comment", () => {
|
|
250
|
+
const r = renderTemplate({
|
|
251
|
+
html: "<aside>{{>hero}}</aside>",
|
|
252
|
+
fields: [moduleField],
|
|
253
|
+
contentValues: { hero: { moduleId: "m", contentInstanceId: "c" } },
|
|
254
|
+
});
|
|
255
|
+
expect(r.html).toContain("<!-- caelo:module hero needs recursive renderer (compose path) -->");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("malformed ref value becomes module-ref-malformed comment + tracks marker", () => {
|
|
259
|
+
const r = renderTemplate({
|
|
260
|
+
html: "<aside>{{>hero}}</aside>",
|
|
261
|
+
fields: [moduleField],
|
|
262
|
+
contentValues: { hero: "not-a-ref" },
|
|
263
|
+
});
|
|
264
|
+
expect(r.html).toContain("<!-- caelo:missing reason=module-ref-malformed hero -->");
|
|
265
|
+
expect(r.missingSlots).toContain("module-ref-malformed:hero");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("renderTemplate — loud-raw on unknown sections (AC #3)", () => {
|
|
270
|
+
it("unknown {{#unknown}}…{{/unknown}} leaves raw markers + tracks field-not-declared", () => {
|
|
271
|
+
const r = renderTemplate({
|
|
272
|
+
html: "<p>{{#unknown}}x{{/unknown}}</p>",
|
|
273
|
+
fields: [],
|
|
274
|
+
contentValues: {},
|
|
275
|
+
});
|
|
276
|
+
expect(r.html).toBe("<p>{{#unknown}}x{{/unknown}}</p>");
|
|
277
|
+
expect(r.missingSlots).toContain("field-not-declared:unknown");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("unknown {{>partial}} leaves raw markers + tracks field-not-declared", () => {
|
|
281
|
+
const r = renderTemplate({
|
|
282
|
+
html: "<aside>{{>ghost}}</aside>",
|
|
283
|
+
fields: [],
|
|
284
|
+
contentValues: {},
|
|
285
|
+
});
|
|
286
|
+
expect(r.html).toBe("<aside>{{>ghost}}</aside>");
|
|
287
|
+
expect(r.missingSlots).toContain("field-not-declared:ghost");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("renderTemplate — whitespace tolerance in markers", () => {
|
|
292
|
+
// The regexes allow surrounding whitespace inside the curly braces
|
|
293
|
+
// (e.g. `{{ name }}`, `{{ #items }}`, `{{ > hero }}`). AI-authored
|
|
294
|
+
// HTML usually emits tight markers, but a future extractor that
|
|
295
|
+
// pretty-prints could regress silently — these tests pin behaviour
|
|
296
|
+
// the regexes already support so any future tightening fails loudly.
|
|
297
|
+
|
|
298
|
+
it("primitive {{ name }} (whitespace inside braces) substitutes normally", () => {
|
|
299
|
+
const r = renderTemplate({
|
|
300
|
+
html: "<h1>{{ title }}</h1>",
|
|
301
|
+
fields: [{ name: "title", kind: "text" }],
|
|
302
|
+
contentValues: { title: "Hello" },
|
|
303
|
+
});
|
|
304
|
+
expect(r.html).toBe("<h1>Hello</h1>");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("section {{ #items }}…{{ /items }} (whitespace inside braces) iterates normally", () => {
|
|
308
|
+
const r = renderTemplate({
|
|
309
|
+
html: "<ul>{{ #items }}<li>{{ . }}</li>{{ /items }}</ul>",
|
|
310
|
+
fields: [{ name: "items", kind: "text-list" }],
|
|
311
|
+
contentValues: { items: ["a", "b"] },
|
|
312
|
+
});
|
|
313
|
+
expect(r.html).toBe("<ul><li>a</li><li>b</li></ul>");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("partial {{ > hero }} (whitespace inside braces) resolves normally", () => {
|
|
317
|
+
const r = renderTemplate({
|
|
318
|
+
html: "<aside>{{ > hero }}</aside>",
|
|
319
|
+
fields: [{ name: "hero", kind: "module" }],
|
|
320
|
+
contentValues: { hero: { moduleId: "m", contentInstanceId: "c" } },
|
|
321
|
+
partials: { hero: "<h1>X</h1>" },
|
|
322
|
+
});
|
|
323
|
+
expect(r.html).toBe("<aside><h1>X</h1></aside>");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("renderTemplate — failure-marker parity", () => {
|
|
328
|
+
// A NON-list field used as a `{{#name}}…{{/name}}` section is a Mustache
|
|
329
|
+
// CONDITIONAL, not a kind-mismatch — the old behaviour silently DROPPED
|
|
330
|
+
// real scalar content (headings, body copy, an image, a nested module).
|
|
331
|
+
it("renders a {{#scalar}} block when the text field has a value", () => {
|
|
332
|
+
const r = renderTemplate({
|
|
333
|
+
html: "<h1>{{#title}}{{title}}{{/title}}</h1>",
|
|
334
|
+
fields: [{ name: "title", kind: "text" }],
|
|
335
|
+
contentValues: { title: "Willkommen" },
|
|
336
|
+
});
|
|
337
|
+
expect(r.html).toBe("<h1>Willkommen</h1>");
|
|
338
|
+
expect(r.missingSlots.some((s) => s.includes("kind-mismatch"))).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("supports {{.}} inside a scalar conditional", () => {
|
|
342
|
+
const r = renderTemplate({
|
|
343
|
+
html: "<h2>{{#title}}{{.}}{{/title}}</h2>",
|
|
344
|
+
fields: [{ name: "title", kind: "text" }],
|
|
345
|
+
contentValues: { title: "Hallo" },
|
|
346
|
+
});
|
|
347
|
+
expect(r.html).toBe("<h2>Hallo</h2>");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("drops a {{#scalar}} block only when the field is empty (conditional, not missing)", () => {
|
|
351
|
+
const r = renderTemplate({
|
|
352
|
+
html: "<p>{{#subtitle}}<span>{{subtitle}}</span>{{/subtitle}}</p>",
|
|
353
|
+
fields: [{ name: "subtitle", kind: "text" }],
|
|
354
|
+
contentValues: {},
|
|
355
|
+
});
|
|
356
|
+
expect(r.html).toBe("<p></p>");
|
|
357
|
+
expect(r.missingSlots).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("wraps a scalar image field in a {{#image}} conditional", () => {
|
|
361
|
+
const r = renderTemplate({
|
|
362
|
+
html: '{{#photo}}<img src="{{photo}}" alt="rep">{{/photo}}',
|
|
363
|
+
fields: [{ name: "photo", kind: "image" }],
|
|
364
|
+
contentValues: { photo: "/_caelo/media/rep-photo" },
|
|
365
|
+
});
|
|
366
|
+
expect(r.html).toBe('<img src="/_caelo/media/rep-photo" alt="rep">');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("kind-mismatch on {{>name}} against a list field", () => {
|
|
370
|
+
const r = renderTemplate({
|
|
371
|
+
html: "<aside>{{>items}}</aside>",
|
|
372
|
+
fields: [{ name: "items", kind: "text-list" }],
|
|
373
|
+
contentValues: {},
|
|
374
|
+
});
|
|
375
|
+
const expected = "kind-mismatch:items expected=module actual=text-list";
|
|
376
|
+
expect(r.html).toBe(`<aside><!-- caelo:missing reason=${expected} --></aside>`);
|
|
377
|
+
expect(r.missingSlots).toContain(expected);
|
|
378
|
+
});
|
|
379
|
+
});
|