@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.
Files changed (184) hide show
  1. package/dist/ai-tools.d.ts +125 -50
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +48 -36
  4. package/dist/ai-tools.js.map +1 -1
  5. package/dist/build-page.d.ts +110 -30
  6. package/dist/build-page.d.ts.map +1 -1
  7. package/dist/build-page.js +23 -4
  8. package/dist/build-page.js.map +1 -1
  9. package/dist/content.d.ts +201 -100
  10. package/dist/content.d.ts.map +1 -1
  11. package/dist/content.js +176 -74
  12. package/dist/content.js.map +1 -1
  13. package/dist/css-var-aliases.d.ts +20 -0
  14. package/dist/css-var-aliases.d.ts.map +1 -0
  15. package/dist/css-var-aliases.js +95 -0
  16. package/dist/css-var-aliases.js.map +1 -0
  17. package/dist/css-var-scan.d.ts +0 -16
  18. package/dist/css-var-scan.d.ts.map +1 -1
  19. package/dist/css-var-scan.js +24 -0
  20. package/dist/css-var-scan.js.map +1 -1
  21. package/dist/edit-markers.d.ts +41 -5
  22. package/dist/edit-markers.d.ts.map +1 -1
  23. package/dist/edit-markers.js +47 -50
  24. package/dist/edit-markers.js.map +1 -1
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/interactions.js +2 -2
  30. package/dist/interactions.js.map +1 -1
  31. package/dist/link-canonicalise.d.ts.map +1 -1
  32. package/dist/link-canonicalise.js +7 -1
  33. package/dist/link-canonicalise.js.map +1 -1
  34. package/dist/link-refs.d.ts +1 -1
  35. package/dist/link-refs.js +3 -3
  36. package/dist/lists/address.d.ts +34 -0
  37. package/dist/lists/address.d.ts.map +1 -0
  38. package/dist/lists/address.js +43 -0
  39. package/dist/lists/address.js.map +1 -0
  40. package/dist/lists/default-types.d.ts +10 -0
  41. package/dist/lists/default-types.d.ts.map +1 -0
  42. package/dist/lists/default-types.js +91 -0
  43. package/dist/lists/default-types.js.map +1 -0
  44. package/dist/lists/fields.d.ts +152 -0
  45. package/dist/lists/fields.d.ts.map +1 -0
  46. package/dist/lists/fields.js +258 -0
  47. package/dist/lists/fields.js.map +1 -0
  48. package/dist/lists/index.d.ts +14 -0
  49. package/dist/lists/index.d.ts.map +1 -0
  50. package/dist/lists/index.js +15 -0
  51. package/dist/lists/index.js.map +1 -0
  52. package/dist/lists/item-attr.d.ts +26 -0
  53. package/dist/lists/item-attr.d.ts.map +1 -0
  54. package/dist/lists/item-attr.js +31 -0
  55. package/dist/lists/item-attr.js.map +1 -0
  56. package/dist/lists/items.d.ts +105 -0
  57. package/dist/lists/items.d.ts.map +1 -0
  58. package/dist/lists/items.js +304 -0
  59. package/dist/lists/items.js.map +1 -0
  60. package/dist/lists/list-type.d.ts +401 -0
  61. package/dist/lists/list-type.d.ts.map +1 -0
  62. package/dist/lists/list-type.js +275 -0
  63. package/dist/lists/list-type.js.map +1 -0
  64. package/dist/lists/model.d.ts +14 -0
  65. package/dist/lists/model.d.ts.map +1 -0
  66. package/dist/lists/model.js +15 -0
  67. package/dist/lists/model.js.map +1 -0
  68. package/dist/lists/page-type.d.ts +115 -0
  69. package/dist/lists/page-type.d.ts.map +1 -0
  70. package/dist/lists/page-type.js +66 -0
  71. package/dist/lists/page-type.js.map +1 -0
  72. package/dist/lists/refs.d.ts +13 -0
  73. package/dist/lists/refs.d.ts.map +1 -0
  74. package/dist/lists/refs.js +18 -0
  75. package/dist/lists/refs.js.map +1 -0
  76. package/dist/lists/render-data.d.ts +114 -0
  77. package/dist/lists/render-data.d.ts.map +1 -0
  78. package/dist/lists/render-data.js +3 -0
  79. package/dist/lists/render-data.js.map +1 -0
  80. package/dist/lists/render-deps.d.ts +68 -0
  81. package/dist/lists/render-deps.d.ts.map +1 -0
  82. package/dist/lists/render-deps.js +91 -0
  83. package/dist/lists/render-deps.js.map +1 -0
  84. package/dist/lists/resolve-values.d.ts +63 -0
  85. package/dist/lists/resolve-values.d.ts.map +1 -0
  86. package/dist/lists/resolve-values.js +181 -0
  87. package/dist/lists/resolve-values.js.map +1 -0
  88. package/dist/lists/resolve.d.ts +106 -0
  89. package/dist/lists/resolve.d.ts.map +1 -0
  90. package/dist/lists/resolve.js +878 -0
  91. package/dist/lists/resolve.js.map +1 -0
  92. package/dist/lists/scope.d.ts +81 -0
  93. package/dist/lists/scope.d.ts.map +1 -0
  94. package/dist/lists/scope.js +3 -0
  95. package/dist/lists/scope.js.map +1 -0
  96. package/dist/markup-structure.d.ts +8 -0
  97. package/dist/markup-structure.d.ts.map +1 -1
  98. package/dist/markup-structure.js +2 -0
  99. package/dist/markup-structure.js.map +1 -1
  100. package/dist/media.d.ts +1 -1
  101. package/dist/nested-module-render.d.ts +25 -20
  102. package/dist/nested-module-render.d.ts.map +1 -1
  103. package/dist/nested-module-render.js +122 -31
  104. package/dist/nested-module-render.js.map +1 -1
  105. package/dist/page-log.d.ts +1 -1
  106. package/dist/preview-compose.d.ts +24 -21
  107. package/dist/preview-compose.d.ts.map +1 -1
  108. package/dist/preview-compose.js +34 -118
  109. package/dist/preview-compose.js.map +1 -1
  110. package/dist/source-text-transport.d.ts +17 -0
  111. package/dist/source-text-transport.d.ts.map +1 -0
  112. package/dist/source-text-transport.js +71 -0
  113. package/dist/source-text-transport.js.map +1 -0
  114. package/dist/template-engine.d.ts +44 -31
  115. package/dist/template-engine.d.ts.map +1 -1
  116. package/dist/template-engine.js +75 -174
  117. package/dist/template-engine.js.map +1 -1
  118. package/dist/template-sections.d.ts +69 -0
  119. package/dist/template-sections.d.ts.map +1 -0
  120. package/dist/template-sections.js +423 -0
  121. package/dist/template-sections.js.map +1 -0
  122. package/dist/version.d.ts +2 -2
  123. package/dist/version.d.ts.map +1 -1
  124. package/dist/version.js +1 -1
  125. package/dist/version.js.map +1 -1
  126. package/package.json +8 -1
  127. package/src/__tests__/redos-hardening.test.ts +50 -16
  128. package/src/ai-tools-add-module-modes.test.ts +50 -0
  129. package/src/ai-tools.test.ts +10 -0
  130. package/src/ai-tools.ts +51 -37
  131. package/src/build-page.test.ts +34 -3
  132. package/src/build-page.ts +26 -3
  133. package/src/content.test.ts +111 -3
  134. package/src/content.ts +186 -75
  135. package/src/css-var-aliases.test.ts +61 -0
  136. package/src/css-var-aliases.ts +109 -0
  137. package/src/css-var-scan.ts +9 -0
  138. package/src/edit-markers.test.ts +37 -21
  139. package/src/edit-markers.ts +52 -20
  140. package/src/index.ts +3 -1
  141. package/src/interactions.ts +2 -2
  142. package/src/link-canonicalise.test.ts +36 -6
  143. package/src/link-canonicalise.ts +6 -1
  144. package/src/link-refs.test.ts +66 -42
  145. package/src/link-refs.ts +3 -3
  146. package/src/lists/address.ts +75 -0
  147. package/src/lists/default-types.ts +102 -0
  148. package/src/lists/fields.ts +285 -0
  149. package/src/lists/index.ts +16 -0
  150. package/src/lists/item-attr.test.ts +36 -0
  151. package/src/lists/item-attr.ts +49 -0
  152. package/src/lists/items.ts +402 -0
  153. package/src/lists/list-fixtures.test.ts +302 -0
  154. package/src/lists/list-type.ts +306 -0
  155. package/src/lists/lists.test.ts +328 -0
  156. package/src/lists/model.ts +16 -0
  157. package/src/lists/page-type.ts +71 -0
  158. package/src/lists/refs.ts +21 -0
  159. package/src/lists/render-data.ts +125 -0
  160. package/src/lists/render-deps.test.ts +132 -0
  161. package/src/lists/render-deps.ts +144 -0
  162. package/src/lists/resolve-values.ts +224 -0
  163. package/src/lists/resolve.test.ts +295 -0
  164. package/src/lists/resolve.ts +1148 -0
  165. package/src/lists/scope.ts +69 -0
  166. package/src/lists-compose-parity.test.ts +262 -0
  167. package/src/markup-structure.ts +10 -0
  168. package/src/nested-module-render.test.ts +111 -77
  169. package/src/nested-module-render.ts +85 -48
  170. package/src/preview-compose.test.ts +55 -19
  171. package/src/preview-compose.ts +59 -154
  172. package/src/source-text-transport.test.ts +63 -0
  173. package/src/source-text-transport.ts +70 -0
  174. package/src/template-engine-lists.test.ts +553 -0
  175. package/src/template-engine.test.ts +62 -80
  176. package/src/template-engine.ts +102 -245
  177. package/src/template-sections.ts +500 -0
  178. package/src/version.ts +1 -1
  179. package/dist/structured-sets.d.ts +0 -70
  180. package/dist/structured-sets.d.ts.map +0 -1
  181. package/dist/structured-sets.js +0 -92
  182. package/dist/structured-sets.js.map +0 -1
  183. package/src/structured-sets.ts +0 -114
  184. package/src/template-engine-data-lists.test.ts +0 -90
@@ -0,0 +1,500 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Section pass of the template engine (`template-engine.ts`): every
5
+ * `{{#name}}…{{/name}}` and `{{^name}}…{{/name}}`, list iteration with
6
+ * entry scopes, `{{>recurse}}`, `{{>f}}` for an entry's embedded module,
7
+ * dot access and `{{page.*}}` (epic #1107, `docs/lists.md` §8).
8
+ *
9
+ * The template is tokenized ONCE and sections are matched with one stack
10
+ * per name, so a children loop may repeat inside itself
11
+ * (`{{#children}}…{{#children}}…{{/children}}…{{/children}}`) and the cost
12
+ * stays linear in the number of tags — no regex backtracks over a section
13
+ * body (CodeQL js/polynomial-redos).
14
+ *
15
+ * Output contract with the later passes: text outside list entries and
16
+ * every tag this pass does not own (a module field's `{{name}}`, a
17
+ * top-level `{{>name}}`, `{{&x}}`, comments) comes out verbatim, so the
18
+ * partial and primitive passes treat it exactly as before. Everything
19
+ * this pass pastes — entry values, rendered nested modules, the original
20
+ * source of a tag that must stay raw — is wrapped in a sentinel (#1053),
21
+ * so content that happens to contain `{{…}}` is never parsed again.
22
+ */
23
+
24
+ import type { ListFieldSpec, ListHolder, ListPageContext } from "./lists/resolve.js";
25
+ import type { ScopeEntry, ScopeValue } from "./lists/scope.js";
26
+
27
+ /** A field as the section pass needs it (subset of `TemplateField`). */
28
+ export interface SectionField extends Partial<Omit<ListFieldSpec, "name" | "default">> {
29
+ readonly name: string;
30
+ readonly kind: string;
31
+ readonly default?: unknown;
32
+ }
33
+
34
+ /** Renders the nested-module partial for `key`; `ref` is set for an entry's module field. */
35
+ export type SectionPartialResolver = (
36
+ key: string,
37
+ ref?: { readonly moduleId: string; readonly contentInstanceId: string },
38
+ ) => string | undefined;
39
+
40
+ /** What the section pass works with — one per `renderTemplate` call. */
41
+ export interface SectionEnv {
42
+ readonly fields: ReadonlyMap<string, SectionField>;
43
+ readonly values: Readonly<Record<string, unknown>>;
44
+ readonly partials: SectionPartialResolver;
45
+ readonly missing: string[];
46
+ readonly mkSentinel: (original: string) => string;
47
+ readonly lists: ListPageContext | undefined;
48
+ readonly holder: ListHolder;
49
+ /** Theme-asset placeholder names, which are module-level keys too. */
50
+ readonly moduleLevelKeys: ReadonlySet<string>;
51
+ /** Renders a `module-list` field (inner block ignored), shared with the old engine path. */
52
+ readonly renderModuleList: (name: string, field: SectionField) => string;
53
+ }
54
+
55
+ /** One `{{…}}` tag of the template. */
56
+ interface Token {
57
+ readonly start: number;
58
+ readonly end: number;
59
+ /** `#`, `^`, `/`, `>` or "" for a plain tag. */
60
+ readonly sigil: string;
61
+ readonly name: string;
62
+ }
63
+
64
+ // `\s*` around the sigil is split by the mandatory sigil character, and
65
+ // the name ends on a word character, so no two quantifiers compete for
66
+ // the same input (linear matching).
67
+ const TAG_RE = /\{\{\s*([#^/>]\s*)?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_]+)*)\s*\}\}/g;
68
+ const SECTION_NAME_RE = /^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*$/;
69
+ const DOT_RE = /\{\{\s*\.\s*\}\}/g;
70
+
71
+ function tokenize(html: string): Token[] {
72
+ const out: Token[] = [];
73
+ TAG_RE.lastIndex = 0;
74
+ for (let m = TAG_RE.exec(html); m !== null; m = TAG_RE.exec(html)) {
75
+ out.push({
76
+ start: m.index,
77
+ end: m.index + m[0].length,
78
+ sigil: (m[1] ?? "").trim(),
79
+ name: m[2] as string,
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Pair every section open with its close: one stack per name, so a name
87
+ * nests inside itself and an unclosed open simply stays unpaired (the
88
+ * later Mustache pass reports it, as before).
89
+ */
90
+ function matchSections(tokens: readonly Token[]): Map<number, number> {
91
+ const pairs = new Map<number, number>();
92
+ const stacks = new Map<string, number[]>();
93
+ tokens.forEach((t, i) => {
94
+ if (!SECTION_NAME_RE.test(t.name)) return;
95
+ if (t.sigil === "#" || t.sigil === "^") {
96
+ const s = stacks.get(t.name) ?? [];
97
+ s.push(i);
98
+ stacks.set(t.name, s);
99
+ } else if (t.sigil === "/") {
100
+ const open = stacks.get(t.name)?.pop();
101
+ if (open !== undefined) pairs.set(open, i);
102
+ }
103
+ });
104
+ return pairs;
105
+ }
106
+
107
+ /** One list entry being rendered, with the block its section iterates. */
108
+ interface Frame {
109
+ readonly entry: ScopeEntry;
110
+ /** Token range of the iterating section's body: [first token, close token). */
111
+ readonly block: {
112
+ readonly from: number;
113
+ readonly to: number;
114
+ readonly textFrom: number;
115
+ readonly textTo: number;
116
+ };
117
+ readonly name: string;
118
+ }
119
+
120
+ interface RenderState {
121
+ readonly chain: readonly Frame[];
122
+ /** `{{.}}` / `{{item}}` value inside a scalar conditional; null elsewhere. */
123
+ readonly dot: string | null;
124
+ }
125
+
126
+ /**
127
+ * Run the section pass over `html`. Returns the HTML with every matched
128
+ * section rendered and every tag inside list entries resolved.
129
+ */
130
+ export function renderSections(html: string, env: SectionEnv): string {
131
+ const tokens = tokenize(html);
132
+ if (tokens.length === 0) return html;
133
+ const pairs = matchSections(tokens);
134
+ if (pairs.size === 0 && !tokens.some((t) => t.name.includes("."))) return html;
135
+ const r = new SectionRenderer(html, tokens, pairs, env);
136
+ return r.range(0, tokens.length, 0, html.length, { chain: [], dot: null });
137
+ }
138
+
139
+ class SectionRenderer {
140
+ constructor(
141
+ private readonly html: string,
142
+ private readonly tokens: readonly Token[],
143
+ private readonly pairs: ReadonlyMap<number, number>,
144
+ private readonly env: SectionEnv,
145
+ ) {}
146
+
147
+ /** Render tokens [from, to) covering text [textFrom, textTo). */
148
+ range(from: number, to: number, textFrom: number, textTo: number, st: RenderState): string {
149
+ let out = "";
150
+ let cursor = textFrom;
151
+ let i = from;
152
+ while (i < to) {
153
+ const t = this.tokens[i] as Token;
154
+ out += this.text(cursor, t.start, st);
155
+ const close = this.pairs.get(i);
156
+ if ((t.sigil === "#" || t.sigil === "^") && close !== undefined && close < to) {
157
+ const c = this.tokens[close] as Token;
158
+ const block = { from: i + 1, to: close, textFrom: t.end, textTo: c.start };
159
+ out += this.section(t, block, this.html.slice(t.start, c.end), st);
160
+ cursor = c.end;
161
+ i = close + 1;
162
+ continue;
163
+ }
164
+ out += this.tag(t, st);
165
+ cursor = t.end;
166
+ i += 1;
167
+ }
168
+ return out + this.text(cursor, textTo, st);
169
+ }
170
+
171
+ private text(from: number, to: number, st: RenderState): string {
172
+ const s = this.html.slice(from, to);
173
+ if (st.dot === null || s.length === 0) return s;
174
+ const dot = st.dot;
175
+ return s.replace(DOT_RE, () => this.env.mkSentinel(dot));
176
+ }
177
+
178
+ private raw(source: string, marker: string | null): string {
179
+ if (marker !== null) this.env.missing.push(marker);
180
+ return this.env.mkSentinel(source);
181
+ }
182
+
183
+ /** The module field a list entry's markers name: the outermost iterated field. */
184
+ private listField(st: RenderState): string {
185
+ return st.chain[0]?.name ?? "";
186
+ }
187
+
188
+ // ── Lookups ──
189
+
190
+ /**
191
+ * Look `name` (possibly dotted) up in the entry chain, innermost
192
+ * first, then `page`. `undefined` = not an entry key (a module-level
193
+ * name, or unknown); `{ missing }` = the head exists but a dotted part
194
+ * does not.
195
+ */
196
+ private lookup(name: string, st: RenderState): ScopeValue | { missing: true } | undefined {
197
+ const parts = name.split(".");
198
+ const head = (parts[0] as string).toLowerCase();
199
+ let v: ScopeValue | undefined;
200
+ if (head === "page") {
201
+ v = this.env.lists?.pageScope() ?? undefined;
202
+ if (v === undefined)
203
+ return parts.length > 1 || st.chain.length > 0 ? { missing: true } : undefined;
204
+ } else {
205
+ for (let k = st.chain.length - 1; k >= 0 && v === undefined; k -= 1) {
206
+ v = st.chain[k]?.entry.get(head);
207
+ }
208
+ if (v === undefined) return undefined;
209
+ }
210
+ for (const p of parts.slice(1)) {
211
+ if (v.kind !== "record") return { missing: true };
212
+ const next: ScopeValue | undefined = v.get(p.toLowerCase());
213
+ if (next === undefined) return { missing: true };
214
+ v = next;
215
+ }
216
+ return v;
217
+ }
218
+
219
+ private isModuleLevel(name: string): boolean {
220
+ const lower = name.toLowerCase();
221
+ return (
222
+ this.env.fields.has(name) || this.env.fields.has(lower) || this.env.moduleLevelKeys.has(lower)
223
+ );
224
+ }
225
+
226
+ private notDeclared(name: string, st: RenderState): string {
227
+ const field = this.listField(st);
228
+ return field.length > 0 ? `field-not-declared:${field}.${name}` : `field-not-declared:${name}`;
229
+ }
230
+
231
+ // ── Plain tags ──
232
+
233
+ private tag(t: Token, st: RenderState): string {
234
+ const source = this.html.slice(t.start, t.end);
235
+ if (t.sigil === ">") return this.partial(t, source, st);
236
+ if (t.sigil !== "") return source;
237
+ if (st.dot !== null && t.name === "item" && st.chain.length === 0) {
238
+ return this.env.mkSentinel(st.dot);
239
+ }
240
+ const dotted = t.name.includes(".");
241
+ if (st.chain.length === 0 && !dotted) return source;
242
+ const v = this.lookup(t.name, st);
243
+ if (v === undefined) {
244
+ if (dotted) {
245
+ const head = t.name.slice(0, t.name.indexOf("."));
246
+ if (st.chain.length === 0 && !this.isModuleLevel(head)) return source;
247
+ return this.raw(source, this.notDeclared(t.name, st));
248
+ }
249
+ if (this.isModuleLevel(t.name)) return source;
250
+ return this.raw(source, this.notDeclared(t.name, st));
251
+ }
252
+ if ("missing" in v) {
253
+ const marker =
254
+ t.name.split(".")[0]?.toLowerCase() === "page" && this.env.lists === undefined
255
+ ? "list-context-unavailable:page"
256
+ : this.notDeclared(t.name, st);
257
+ return this.raw(source, marker);
258
+ }
259
+ return this.valueText(v, source, t.name, st);
260
+ }
261
+
262
+ private valueText(v: ScopeValue, source: string, name: string, st: RenderState): string {
263
+ switch (v.kind) {
264
+ case "text":
265
+ return v.text.length === 0 ? "" : this.env.mkSentinel(v.text);
266
+ case "bool":
267
+ return String(v.value);
268
+ case "record":
269
+ if (v.text === null) return this.raw(source, this.notDeclared(name, st));
270
+ return v.text.length === 0 ? "" : this.env.mkSentinel(v.text);
271
+ case "failed":
272
+ return this.raw(source, v.marker);
273
+ default:
274
+ return this.raw(source, this.notDeclared(name, st));
275
+ }
276
+ }
277
+
278
+ // ── Partials ──
279
+
280
+ private partial(t: Token, source: string, st: RenderState): string {
281
+ if (st.chain.length === 0) return source;
282
+ if (t.name === "recurse") return this.recurse(st);
283
+ const v = this.lookup(t.name, st);
284
+ if (v === undefined) {
285
+ return this.isModuleLevel(t.name) ? source : this.raw(source, this.notDeclared(t.name, st));
286
+ }
287
+ if ("missing" in v) return this.raw(source, this.notDeclared(t.name, st));
288
+ if (v.kind === "module") {
289
+ const frame = st.chain[st.chain.length - 1] as Frame;
290
+ const field = this.listField(st);
291
+ const key = `list:${field}/${frame.entry.id}/${t.name}`;
292
+ const html = this.env.partials(key, v.ref);
293
+ if (html === undefined) {
294
+ this.env.missing.push(`nested-renderer-unavailable:${field}[${frame.entry.id}].${t.name}`);
295
+ return `<!-- caelo:module ${field}[${frame.entry.id}].${t.name} needs recursive renderer (compose path) -->`;
296
+ }
297
+ return this.env.mkSentinel(html);
298
+ }
299
+ if (v.kind === "text" && v.text.length === 0) return "";
300
+ if (v.kind === "failed") return this.raw(source, v.marker);
301
+ const reason = `kind-mismatch:${this.listField(st)}.${t.name} expected=module actual=${v.kind}`;
302
+ this.env.missing.push(reason);
303
+ return `<!-- caelo:missing reason=${reason} -->`;
304
+ }
305
+
306
+ /** `{{>recurse}}`: the current entry's children with the enclosing section's block. */
307
+ private recurse(st: RenderState): string {
308
+ const frame = st.chain[st.chain.length - 1] as Frame;
309
+ const key = frame.entry.childrenKey;
310
+ if (key === null) return "";
311
+ const kids = frame.entry.get(key);
312
+ if (kids?.kind !== "list") return "";
313
+ return this.iterate(kids.entries, frame.name, frame.block, st);
314
+ }
315
+
316
+ // ── Sections ──
317
+
318
+ private section(t: Token, block: Frame["block"], source: string, st: RenderState): string {
319
+ const inverted = t.sigil === "^";
320
+ const v = this.lookup(t.name, st);
321
+ if (v !== undefined) {
322
+ if ("missing" in v) {
323
+ const marker =
324
+ t.name.split(".")[0] === "page" && this.env.lists === undefined
325
+ ? "list-context-unavailable:page"
326
+ : this.notDeclared(t.name, st);
327
+ return this.raw(source, marker);
328
+ }
329
+ return this.scopeSection(v, t.name, inverted, block, source, st);
330
+ }
331
+ if (t.name.includes(".")) return this.raw(source, this.notDeclared(t.name, st));
332
+ return this.moduleSection(t.name, inverted, block, source, st);
333
+ }
334
+
335
+ /** A section over a value found in the entry chain or `page`. */
336
+ private scopeSection(
337
+ v: ScopeValue,
338
+ name: string,
339
+ inverted: boolean,
340
+ block: Frame["block"],
341
+ source: string,
342
+ st: RenderState,
343
+ ): string {
344
+ if (v.kind === "failed") return this.raw(source, v.marker);
345
+ if (v.kind === "list") {
346
+ if (inverted) return v.entries.length === 0 ? this.body(block, st, null) : "";
347
+ return this.iterate(v.entries, name, block, st);
348
+ }
349
+ const present =
350
+ v.kind === "bool"
351
+ ? v.value
352
+ : v.kind === "text"
353
+ ? v.text.trim().length > 0
354
+ : v.kind === "record"
355
+ ? v.present
356
+ : true;
357
+ return present !== inverted ? this.body(block, st, st.dot) : "";
358
+ }
359
+
360
+ /** A section over a module field (or an undeclared plugin source name). */
361
+ private moduleSection(
362
+ name: string,
363
+ inverted: boolean,
364
+ block: Frame["block"],
365
+ source: string,
366
+ st: RenderState,
367
+ ): string {
368
+ const env = this.env;
369
+ const field = env.fields.get(name);
370
+ if (!field) {
371
+ // No declared field — a plugin may still offer this name as a list
372
+ // source. Module fields are looked up FIRST, so a plugin can never
373
+ // shadow something the module's author declared.
374
+ const plugin = env.lists?.pluginSource(name) ?? null;
375
+ if (plugin !== null) {
376
+ if (!plugin.ok) return this.raw(source, plugin.marker);
377
+ if (inverted) return plugin.entries.length === 0 ? this.body(block, st, null) : "";
378
+ return this.iterate(plugin.entries, name, block, st);
379
+ }
380
+ return this.raw(source, this.notDeclared(name, st));
381
+ }
382
+ const raw = Object.hasOwn(env.values, name) ? env.values[name] : field.default;
383
+ if (field.kind === "module-list") {
384
+ if (inverted) return Array.isArray(raw) && raw.length > 0 ? "" : this.body(block, st, null);
385
+ return env.renderModuleList(name, field);
386
+ }
387
+ if (field.kind === "list") {
388
+ if (!env.lists) return this.raw(source, `list-context-unavailable:${name}`);
389
+ const spec: ListFieldSpec = {
390
+ name: field.name,
391
+ listType: field.listType ?? "",
392
+ ...(field.bind !== undefined ? { bind: field.bind } : {}),
393
+ ...(field.list !== undefined ? { list: field.list } : {}),
394
+ ...(field.source !== undefined ? { source: field.source } : {}),
395
+ ...(field.default !== undefined ? { default: field.default } : {}),
396
+ };
397
+ const value = Object.hasOwn(env.values, name) ? env.values[name] : undefined;
398
+ const res = env.lists.resolveField(spec, value, env.holder);
399
+ if (!res.ok) return this.raw(source, res.marker);
400
+ if (inverted) return res.entries.length === 0 ? this.body(block, st, null) : "";
401
+ return this.iterate(res.entries, name, block, st);
402
+ }
403
+ // Any other field used as a section is a CONDITIONAL (Mustache
404
+ // semantics): the block once when the field has a present, non-empty
405
+ // value, dropped when empty — never a kind-mismatch, which used to
406
+ // silently drop real scalar content. `{{.}}` / `{{item}}` inside it
407
+ // read the scalar. An empty conditional is intentional, not missing.
408
+ const present = isPresentSectionValue(raw);
409
+ if (present === inverted) return "";
410
+ const dot = present ? scalarSectionString(raw) : "";
411
+ return this.body(block, st, dot.length > 0 ? dot : st.dot);
412
+ }
413
+
414
+ private body(block: Frame["block"], st: RenderState, dot: string | null): string {
415
+ return this.range(block.from, block.to, block.textFrom, block.textTo, { chain: st.chain, dot });
416
+ }
417
+
418
+ private iterate(
419
+ entries: readonly ScopeEntry[],
420
+ name: string,
421
+ block: Frame["block"],
422
+ st: RenderState,
423
+ ): string {
424
+ // The empty-restoring sentinel in front claims the iteration's first
425
+ // element for its own entries: an enclosing entry whose block starts
426
+ // with this iteration (`{{#items}}<p>…`) must not stamp a GENERATED
427
+ // entry's element as its own root.
428
+ let out = entries.length > 0 ? this.env.mkSentinel("") : "";
429
+ for (const entry of entries) {
430
+ const chain = [...st.chain, { entry, block, name }];
431
+ const rendered = this.range(block.from, block.to, block.textFrom, block.textTo, {
432
+ chain,
433
+ dot: null,
434
+ });
435
+ out += entry.itemAttr === null ? rendered : stampListItem(rendered, entry.itemAttr);
436
+ }
437
+ return out;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Mustache-style truthiness for a conditional section value. Empty string,
443
+ * null/undefined, `false`, and an empty array/object are absent; everything
444
+ * else (incl. a numeric 0 — a real content value, not a control flag) is
445
+ * present.
446
+ */
447
+ export function isPresentSectionValue(raw: unknown): boolean {
448
+ if (raw === undefined || raw === null || raw === false) return false;
449
+ if (typeof raw === "string") return raw.trim().length > 0;
450
+ if (typeof raw === "number") return !Number.isNaN(raw);
451
+ if (Array.isArray(raw)) return raw.length > 0;
452
+ if (typeof raw === "object") return Object.keys(raw as object).length > 0;
453
+ return true;
454
+ }
455
+
456
+ /** String form for `{{.}}`/`{{item}}` inside a scalar conditional; objects stringify to "". */
457
+ function scalarSectionString(raw: unknown): string {
458
+ if (typeof raw === "string") return raw;
459
+ if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
460
+ return "";
461
+ }
462
+
463
+ /** The attribute a rendered list entry's root element carries (`docs/lists.md` §13). */
464
+ export const LIST_ITEM_ATTRIBUTE = "data-caelo-list-item";
465
+
466
+ /**
467
+ * Stamp `data-caelo-list-item` on a rendered entry's root element: the
468
+ * first opening tag, when the entry's markup STARTS with one (leading
469
+ * whitespace and comments allowed). An entry that starts with text
470
+ * (`{{name}}, `) has no root element and stays unstamped, rather than
471
+ * attributing the entry to some inner `<b>`. Idempotent.
472
+ */
473
+ export function stampListItem(html: string, attr: string): string {
474
+ let i = 0;
475
+ for (;;) {
476
+ while (i < html.length && /\s/.test(html[i] as string)) i += 1;
477
+ if (html.startsWith("<!--", i)) {
478
+ const end = html.indexOf("-->", i + 4);
479
+ if (end < 0) return html;
480
+ i = end + 3;
481
+ continue;
482
+ }
483
+ break;
484
+ }
485
+ if (html[i] !== "<" || !/[a-zA-Z]/.test(html[i + 1] ?? "")) return html;
486
+ let quote: string | null = null;
487
+ let j = i + 1;
488
+ for (; j < html.length; j += 1) {
489
+ const c = html[j];
490
+ if (quote) {
491
+ if (c === quote) quote = null;
492
+ } else if (c === '"' || c === "'") quote = c;
493
+ else if (c === ">") break;
494
+ }
495
+ if (j >= html.length) return html;
496
+ const tag = html.slice(i, j);
497
+ if (tag.includes(`${LIST_ITEM_ATTRIBUTE}=`)) return html;
498
+ const insertAt = tag.endsWith("/") ? j - 1 : j;
499
+ return `${html.slice(0, insertAt)} ${LIST_ITEM_ATTRIBUTE}="${attr.replace(/"/g, "&quot;")}"${html.slice(insertAt)}`;
500
+ }
package/src/version.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * follow standard SemVer.
23
23
  */
24
24
 
25
- export const CAELO_VERSION = "0.10.72";
25
+ export const CAELO_VERSION = "0.11.0";
26
26
 
27
27
  /**
28
28
  * Deprecated alias for back-compat — early P17 work spelled this
@@ -1,70 +0,0 @@
1
- /**
2
- * P6.7.5 — typed schemas for the `structured_sets` table.
3
- *
4
- * One database table holds N kinds of named lists (nav-menus, taxonomies,
5
- * theme tokens, tag lists, footer link blocks, ...). The `kind`
6
- * discriminator drives both the Zod validator at the Query API layer
7
- * and the renderer that turns the items into HTML at preview / deploy.
8
- *
9
- * Adding a new kind: add a Zod schema below, add it to the
10
- * `structuredSetItem` discriminated union, add a renderer in
11
- * `packages/shared/src/structured-set-renderer.ts`. No DB migration
12
- * required — the table already accepts arbitrary `kind` text.
13
- *
14
- * Item shapes are deliberately **structured data**, not HTML strings.
15
- * That's what makes `change_page_slug` able to retarget every menu
16
- * link automatically: walk the JSON, swap `href` matches.
17
- */
18
- import { z } from "zod";
19
- /** Recursive nav menu — supports arbitrary submenu nesting. */
20
- export declare const navMenuItem: z.ZodType<{
21
- label: string;
22
- href: string;
23
- target?: "_self" | "_blank";
24
- children?: {
25
- label: string;
26
- href: string;
27
- }[];
28
- /** When set, the renderer delegates this slot to the named ad plugin. */
29
- adSlotId?: string;
30
- }>;
31
- /** Tree-shaped category list. Used by typed-content / tag pages (P12A). */
32
- export declare const taxonomyItem: z.ZodObject<{
33
- slug: z.ZodString;
34
- displayName: z.ZodString;
35
- parentSlug: z.ZodOptional<z.ZodString>;
36
- description: z.ZodOptional<z.ZodString>;
37
- }, z.core.$strict>;
38
- /** Flat tag — used by post taxonomies, content filtering. */
39
- export declare const tagItem: z.ZodObject<{
40
- slug: z.ZodString;
41
- displayName: z.ZodString;
42
- color: z.ZodOptional<z.ZodString>;
43
- }, z.core.$strict>;
44
- /** Footer "legal" / related-resources block. */
45
- export declare const linkListItem: z.ZodObject<{
46
- label: z.ZodString;
47
- href: z.ZodString;
48
- description: z.ZodOptional<z.ZodString>;
49
- }, z.core.$strict>;
50
- /** Discriminated kind → items array. The Query API op picks the
51
- * validator at runtime by reading `kind` first.
52
- *
53
- * v0.11.0 (#45) — `theme` is no longer a structured-set kind. The
54
- * theme primitive moved to its own `themes` table with DTCG-shaped
55
- * jsonb tokens; see `themes.ts` + `theme-render.ts`. */
56
- export declare const structuredSetKind: z.ZodEnum<{
57
- "link-list": "link-list";
58
- "nav-menu": "nav-menu";
59
- taxonomy: "taxonomy";
60
- tags: "tags";
61
- }>;
62
- export type StructuredSetKind = z.infer<typeof structuredSetKind>;
63
- /** Validate `items` against the right schema for `kind`. Throws a
64
- * ZodError on mismatch — callers wrap and return `Err(ValidationFailed)`. */
65
- export declare function validateStructuredSetItems(kind: StructuredSetKind, items: unknown): unknown[];
66
- export type NavMenuItem = z.infer<typeof navMenuItem>;
67
- export type TaxonomyItem = z.infer<typeof taxonomyItem>;
68
- export type TagItem = z.infer<typeof tagItem>;
69
- export type LinkListItem = z.infer<typeof linkListItem>;
70
- //# sourceMappingURL=structured-sets.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structured-sets.d.ts","sourceRoot":"","sources":["../src/structured-sets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAUA,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,YAAY;;;;;kBAOd,CAAC;AAEZ,6DAA6D;AAC7D,eAAO,MAAM,OAAO;;;;kBAaT,CAAC;AAEZ,gDAAgD;AAChD,eAAO,MAAM,YAAY;;;;kBAMd,CAAC;AAEZ;;;;;yDAKyD;AACzD,eAAO,MAAM,iBAAiB;;;;;EAAwD,CAAC;AACvF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;8EAC8E;AAC9E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAoB7F;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACtD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC"}
@@ -1,92 +0,0 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
- /**
3
- * P6.7.5 — typed schemas for the `structured_sets` table.
4
- *
5
- * One database table holds N kinds of named lists (nav-menus, taxonomies,
6
- * theme tokens, tag lists, footer link blocks, ...). The `kind`
7
- * discriminator drives both the Zod validator at the Query API layer
8
- * and the renderer that turns the items into HTML at preview / deploy.
9
- *
10
- * Adding a new kind: add a Zod schema below, add it to the
11
- * `structuredSetItem` discriminated union, add a renderer in
12
- * `packages/shared/src/structured-set-renderer.ts`. No DB migration
13
- * required — the table already accepts arbitrary `kind` text.
14
- *
15
- * Item shapes are deliberately **structured data**, not HTML strings.
16
- * That's what makes `change_page_slug` able to retarget every menu
17
- * link automatically: walk the JSON, swap `href` matches.
18
- */
19
- import { z } from "zod";
20
- /** Recursive nav menu — supports arbitrary submenu nesting. */
21
- export const navMenuItem = z.lazy(() => z
22
- .object({
23
- label: z.string().min(1).max(120),
24
- href: z.string().min(1).max(500),
25
- target: z.enum(["_self", "_blank"]).optional(),
26
- children: z.array(navMenuItem).optional(),
27
- adSlotId: z.string().min(1).max(100).optional(),
28
- })
29
- .strict());
30
- /** Tree-shaped category list. Used by typed-content / tag pages (P12A). */
31
- export const taxonomyItem = z
32
- .object({
33
- slug: z.string().min(1).max(120),
34
- displayName: z.string().min(1).max(200),
35
- parentSlug: z.string().min(1).max(120).optional(),
36
- description: z.string().max(2000).optional(),
37
- })
38
- .strict();
39
- /** Flat tag — used by post taxonomies, content filtering. */
40
- export const tagItem = z
41
- .object({
42
- slug: z
43
- .string()
44
- .min(1)
45
- .max(120)
46
- .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"),
47
- displayName: z.string().min(1).max(200),
48
- color: z
49
- .string()
50
- .regex(/^#[0-9a-fA-F]{3,8}$/, "hex color")
51
- .optional(),
52
- })
53
- .strict();
54
- /** Footer "legal" / related-resources block. */
55
- export const linkListItem = z
56
- .object({
57
- label: z.string().min(1).max(200),
58
- href: z.string().min(1).max(500),
59
- description: z.string().max(500).optional(),
60
- })
61
- .strict();
62
- /** Discriminated kind → items array. The Query API op picks the
63
- * validator at runtime by reading `kind` first.
64
- *
65
- * v0.11.0 (#45) — `theme` is no longer a structured-set kind. The
66
- * theme primitive moved to its own `themes` table with DTCG-shaped
67
- * jsonb tokens; see `themes.ts` + `theme-render.ts`. */
68
- export const structuredSetKind = z.enum(["nav-menu", "taxonomy", "tags", "link-list"]);
69
- /** Validate `items` against the right schema for `kind`. Throws a
70
- * ZodError on mismatch — callers wrap and return `Err(ValidationFailed)`. */
71
- export function validateStructuredSetItems(kind, items) {
72
- if (!Array.isArray(items)) {
73
- throw new z.ZodError([
74
- {
75
- code: "custom",
76
- path: ["items"],
77
- message: "items must be an array",
78
- },
79
- ]);
80
- }
81
- switch (kind) {
82
- case "nav-menu":
83
- return items.map((it) => navMenuItem.parse(it));
84
- case "taxonomy":
85
- return items.map((it) => taxonomyItem.parse(it));
86
- case "tags":
87
- return items.map((it) => tagItem.parse(it));
88
- case "link-list":
89
- return items.map((it) => linkListItem.parse(it));
90
- }
91
- }
92
- //# sourceMappingURL=structured-sets.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structured-sets.js","sourceRoot":"","sources":["../src/structured-sets.ts"],"names":[],"mappings":"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"}