@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
|
@@ -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, """)}"${html.slice(insertAt)}`;
|
|
500
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -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"}
|
package/dist/structured-sets.js
DELETED
|
@@ -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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"}
|