@caelo-cms/shared 0.10.71 → 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 (202) 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 +50 -38
  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 +26 -7
  8. package/dist/build-page.js.map +1 -1
  9. package/dist/content.d.ts +203 -102
  10. package/dist/content.d.ts.map +1 -1
  11. package/dist/content.js +178 -76
  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/id-safe-truncate.d.ts +15 -0
  26. package/dist/id-safe-truncate.d.ts.map +1 -0
  27. package/dist/id-safe-truncate.js +47 -0
  28. package/dist/id-safe-truncate.js.map +1 -0
  29. package/dist/index.d.ts +5 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/interactions.js +2 -2
  34. package/dist/interactions.js.map +1 -1
  35. package/dist/link-canonicalise.d.ts.map +1 -1
  36. package/dist/link-canonicalise.js +7 -1
  37. package/dist/link-canonicalise.js.map +1 -1
  38. package/dist/link-refs.d.ts +1 -1
  39. package/dist/link-refs.js +3 -3
  40. package/dist/lists/address.d.ts +34 -0
  41. package/dist/lists/address.d.ts.map +1 -0
  42. package/dist/lists/address.js +43 -0
  43. package/dist/lists/address.js.map +1 -0
  44. package/dist/lists/default-types.d.ts +10 -0
  45. package/dist/lists/default-types.d.ts.map +1 -0
  46. package/dist/lists/default-types.js +91 -0
  47. package/dist/lists/default-types.js.map +1 -0
  48. package/dist/lists/fields.d.ts +152 -0
  49. package/dist/lists/fields.d.ts.map +1 -0
  50. package/dist/lists/fields.js +258 -0
  51. package/dist/lists/fields.js.map +1 -0
  52. package/dist/lists/index.d.ts +14 -0
  53. package/dist/lists/index.d.ts.map +1 -0
  54. package/dist/lists/index.js +15 -0
  55. package/dist/lists/index.js.map +1 -0
  56. package/dist/lists/item-attr.d.ts +26 -0
  57. package/dist/lists/item-attr.d.ts.map +1 -0
  58. package/dist/lists/item-attr.js +31 -0
  59. package/dist/lists/item-attr.js.map +1 -0
  60. package/dist/lists/items.d.ts +105 -0
  61. package/dist/lists/items.d.ts.map +1 -0
  62. package/dist/lists/items.js +304 -0
  63. package/dist/lists/items.js.map +1 -0
  64. package/dist/lists/list-type.d.ts +401 -0
  65. package/dist/lists/list-type.d.ts.map +1 -0
  66. package/dist/lists/list-type.js +275 -0
  67. package/dist/lists/list-type.js.map +1 -0
  68. package/dist/lists/model.d.ts +14 -0
  69. package/dist/lists/model.d.ts.map +1 -0
  70. package/dist/lists/model.js +15 -0
  71. package/dist/lists/model.js.map +1 -0
  72. package/dist/lists/page-type.d.ts +115 -0
  73. package/dist/lists/page-type.d.ts.map +1 -0
  74. package/dist/lists/page-type.js +66 -0
  75. package/dist/lists/page-type.js.map +1 -0
  76. package/dist/lists/refs.d.ts +13 -0
  77. package/dist/lists/refs.d.ts.map +1 -0
  78. package/dist/lists/refs.js +18 -0
  79. package/dist/lists/refs.js.map +1 -0
  80. package/dist/lists/render-data.d.ts +114 -0
  81. package/dist/lists/render-data.d.ts.map +1 -0
  82. package/dist/lists/render-data.js +3 -0
  83. package/dist/lists/render-data.js.map +1 -0
  84. package/dist/lists/render-deps.d.ts +68 -0
  85. package/dist/lists/render-deps.d.ts.map +1 -0
  86. package/dist/lists/render-deps.js +91 -0
  87. package/dist/lists/render-deps.js.map +1 -0
  88. package/dist/lists/resolve-values.d.ts +63 -0
  89. package/dist/lists/resolve-values.d.ts.map +1 -0
  90. package/dist/lists/resolve-values.js +181 -0
  91. package/dist/lists/resolve-values.js.map +1 -0
  92. package/dist/lists/resolve.d.ts +106 -0
  93. package/dist/lists/resolve.d.ts.map +1 -0
  94. package/dist/lists/resolve.js +878 -0
  95. package/dist/lists/resolve.js.map +1 -0
  96. package/dist/lists/scope.d.ts +81 -0
  97. package/dist/lists/scope.d.ts.map +1 -0
  98. package/dist/lists/scope.js +3 -0
  99. package/dist/lists/scope.js.map +1 -0
  100. package/dist/markup-structure.d.ts +8 -0
  101. package/dist/markup-structure.d.ts.map +1 -1
  102. package/dist/markup-structure.js +2 -0
  103. package/dist/markup-structure.js.map +1 -1
  104. package/dist/media.d.ts +1 -1
  105. package/dist/nested-module-render.d.ts +25 -20
  106. package/dist/nested-module-render.d.ts.map +1 -1
  107. package/dist/nested-module-render.js +122 -31
  108. package/dist/nested-module-render.js.map +1 -1
  109. package/dist/page-log.d.ts +1 -1
  110. package/dist/preview-compose.d.ts +24 -21
  111. package/dist/preview-compose.d.ts.map +1 -1
  112. package/dist/preview-compose.js +34 -118
  113. package/dist/preview-compose.js.map +1 -1
  114. package/dist/preview-scanner.d.ts +5 -2
  115. package/dist/preview-scanner.d.ts.map +1 -1
  116. package/dist/preview-scanner.js +5 -3
  117. package/dist/preview-scanner.js.map +1 -1
  118. package/dist/source-text-transport.d.ts +17 -0
  119. package/dist/source-text-transport.d.ts.map +1 -0
  120. package/dist/source-text-transport.js +71 -0
  121. package/dist/source-text-transport.js.map +1 -0
  122. package/dist/template-engine.d.ts +44 -31
  123. package/dist/template-engine.d.ts.map +1 -1
  124. package/dist/template-engine.js +75 -174
  125. package/dist/template-engine.js.map +1 -1
  126. package/dist/template-sections.d.ts +69 -0
  127. package/dist/template-sections.d.ts.map +1 -0
  128. package/dist/template-sections.js +423 -0
  129. package/dist/template-sections.js.map +1 -0
  130. package/dist/uuid-diagnose.d.ts +9 -0
  131. package/dist/uuid-diagnose.d.ts.map +1 -0
  132. package/dist/uuid-diagnose.js +47 -0
  133. package/dist/uuid-diagnose.js.map +1 -0
  134. package/dist/version.d.ts +2 -2
  135. package/dist/version.d.ts.map +1 -1
  136. package/dist/version.js +1 -1
  137. package/dist/version.js.map +1 -1
  138. package/package.json +8 -1
  139. package/src/__tests__/redos-hardening.test.ts +50 -16
  140. package/src/ai-tools-add-module-modes.test.ts +50 -0
  141. package/src/ai-tools.test.ts +10 -0
  142. package/src/ai-tools.ts +53 -39
  143. package/src/build-page.test.ts +34 -3
  144. package/src/build-page.ts +29 -6
  145. package/src/content.test.ts +111 -3
  146. package/src/content.ts +188 -77
  147. package/src/css-var-aliases.test.ts +61 -0
  148. package/src/css-var-aliases.ts +109 -0
  149. package/src/css-var-scan.ts +9 -0
  150. package/src/edit-markers.test.ts +37 -21
  151. package/src/edit-markers.ts +52 -20
  152. package/src/id-safe-truncate.test.ts +35 -0
  153. package/src/id-safe-truncate.ts +46 -0
  154. package/src/index.ts +5 -1
  155. package/src/interactions.ts +2 -2
  156. package/src/link-canonicalise.test.ts +36 -6
  157. package/src/link-canonicalise.ts +6 -1
  158. package/src/link-refs.test.ts +66 -42
  159. package/src/link-refs.ts +3 -3
  160. package/src/lists/address.ts +75 -0
  161. package/src/lists/default-types.ts +102 -0
  162. package/src/lists/fields.ts +285 -0
  163. package/src/lists/index.ts +16 -0
  164. package/src/lists/item-attr.test.ts +36 -0
  165. package/src/lists/item-attr.ts +49 -0
  166. package/src/lists/items.ts +402 -0
  167. package/src/lists/list-fixtures.test.ts +302 -0
  168. package/src/lists/list-type.ts +306 -0
  169. package/src/lists/lists.test.ts +328 -0
  170. package/src/lists/model.ts +16 -0
  171. package/src/lists/page-type.ts +71 -0
  172. package/src/lists/refs.ts +21 -0
  173. package/src/lists/render-data.ts +125 -0
  174. package/src/lists/render-deps.test.ts +132 -0
  175. package/src/lists/render-deps.ts +144 -0
  176. package/src/lists/resolve-values.ts +224 -0
  177. package/src/lists/resolve.test.ts +295 -0
  178. package/src/lists/resolve.ts +1148 -0
  179. package/src/lists/scope.ts +69 -0
  180. package/src/lists-compose-parity.test.ts +262 -0
  181. package/src/markup-structure.ts +10 -0
  182. package/src/nested-module-render.test.ts +111 -77
  183. package/src/nested-module-render.ts +85 -48
  184. package/src/preview-compose.test.ts +74 -19
  185. package/src/preview-compose.ts +59 -154
  186. package/src/preview-scanner.test.ts +8 -4
  187. package/src/preview-scanner.ts +5 -3
  188. package/src/source-text-transport.test.ts +63 -0
  189. package/src/source-text-transport.ts +70 -0
  190. package/src/template-engine-lists.test.ts +553 -0
  191. package/src/template-engine.test.ts +62 -80
  192. package/src/template-engine.ts +102 -245
  193. package/src/template-sections.ts +500 -0
  194. package/src/uuid-diagnose.test.ts +28 -0
  195. package/src/uuid-diagnose.ts +50 -0
  196. package/src/version.ts +1 -1
  197. package/dist/structured-sets.d.ts +0 -70
  198. package/dist/structured-sets.d.ts.map +0 -1
  199. package/dist/structured-sets.js +0 -92
  200. package/dist/structured-sets.js.map +0 -1
  201. package/src/structured-sets.ts +0 -114
  202. package/src/template-engine-data-lists.test.ts +0 -90
@@ -25,10 +25,31 @@ import {
25
25
  parseLinkRef,
26
26
  resolveLinkRefsInHtml,
27
27
  } from "./link-refs.js";
28
+ import { DEFAULT_LIST_TYPES } from "./lists/default-types.js";
29
+ import { createListRenderContext } from "./lists/resolve.js";
28
30
  import { renderPlacedModule } from "./nested-module-render.js";
29
31
  import { composePageWithLayout } from "./preview-compose.js";
30
32
  import { renderTemplate } from "./template-engine.js";
31
33
 
34
+ /** A list context that knows only the shipped list types (local lists need nothing else). */
35
+ const lists = createListRenderContext({
36
+ listTypes: new Map(DEFAULT_LIST_TYPES.map((t, i) => [t.slug, { id: `t${i}`, def: t }])),
37
+ lists: new Map(),
38
+ pageTypes: new Map(),
39
+ pages: new Map(),
40
+ media: new Map(),
41
+ siteLanguage: "en",
42
+ }).forPage(null);
43
+ const uid = (n: number) => `00000000-0000-4000-9000-${String(n).padStart(12, "0")}`;
44
+ const links = (...ls: [string, string][]) =>
45
+ ls.map(([label, href], i) => ({
46
+ id: uid(i + 1),
47
+ node: "link",
48
+ values: { label, link: { href } },
49
+ }));
50
+ const unstamp = (html: string) => html.replace(/ data-caelo-list-item="[^"]*"/g, "");
51
+ const NAV_HTML = '<nav>{{#nav}}<a href="{{link}}">{{label}}</a>{{/nav}}</nav>';
52
+
32
53
  const PAGE = "11111111-1111-4111-8111-111111111111";
33
54
  const DRAFT = "22222222-2222-4222-8222-222222222222";
34
55
  const GONE = "33333333-3333-4333-8333-333333333333";
@@ -172,25 +193,26 @@ describe("resolution", () => {
172
193
  describe("template engine — references resolve wherever they come from", () => {
173
194
  const resolver = () => index().forPage(null);
174
195
 
175
- it("resolves module HTML attributes, values, defaults and link-list items", () => {
196
+ it("resolves module HTML attributes, values, defaults and list entries", () => {
176
197
  const r = renderTemplate({
177
- html: `<a href="caelo:page/${PAGE}">Home</a><a href="{{cta_href}}">{{cta_label}}</a><img src="{{hero}}">{{#nav}}<a href="{{href}}">{{label}}</a>{{/nav}}<div>{{body}}</div>`,
198
+ html: `<a href="caelo:page/${PAGE}">Home</a><a href="{{cta_href}}">{{cta_label}}</a><img src="{{hero}}">{{#nav}}<a href="{{link}}">{{label}}</a>{{/nav}}<div>{{body}}</div>`,
178
199
  fields: [
179
200
  { name: "cta_href", kind: "url", default: `caelo:page/${PAGE}#contact` },
180
201
  { name: "cta_label", kind: "text" },
181
202
  { name: "hero", kind: "image" },
182
- { name: "nav", kind: "link-list" },
203
+ { name: "nav", kind: "list", listType: "links" },
183
204
  { name: "body", kind: "richtext" },
184
205
  ],
185
206
  contentValues: {
186
207
  cta_label: "Talk to us",
187
208
  hero: `caelo:media/${MEDIA}`,
188
- nav: [{ label: "Pricing", href: `caelo:page/${DRAFT}` }],
209
+ nav: links(["Pricing", `caelo:page/${DRAFT}`]),
189
210
  body: `<p>See <a href="caelo:page/${PAGE}?x=1">about</a></p>`,
190
211
  },
212
+ lists,
191
213
  linkResolver: resolver(),
192
214
  });
193
- expect(r.html).toBe(
215
+ expect(unstamp(r.html)).toBe(
194
216
  '<a href="/company/about/">Home</a><a href="/company/about/#contact">Talk to us</a>' +
195
217
  '<img src="/_caelo/media/hero-shot"><a href="/pricing/">Pricing</a>' +
196
218
  '<div><p>See <a href="/company/about/?x=1">about</a></p></div>',
@@ -241,8 +263,8 @@ describe("template engine — references resolve wherever they come from", () =>
241
263
  });
242
264
  });
243
265
 
244
- describe("composer — nav-menu sets and pre-rendered modules", () => {
245
- it("resolves nav-menu item hrefs and attributes a broken one to the nav module", () => {
266
+ describe("composer — chrome lists and pre-rendered modules", () => {
267
+ it("resolves a chrome module's list entries and attributes a broken one to the module", () => {
246
268
  const out = composePageWithLayout({
247
269
  layoutHtml:
248
270
  '<html><head></head><body><caelo-slot name="header"></caelo-slot><caelo-slot name="content"></caelo-slot></body></html>',
@@ -253,11 +275,19 @@ describe("composer — nav-menu sets and pre-rendered modules", () => {
253
275
  modules: [
254
276
  {
255
277
  moduleId: "m-nav",
256
- slug: "nav-menu-main",
278
+ slug: "main-nav",
257
279
  displayName: "Nav",
258
- html: "",
280
+ html: NAV_HTML,
259
281
  css: "",
260
282
  js: "",
283
+ fields: [
284
+ {
285
+ name: "nav",
286
+ kind: "list",
287
+ listType: "links",
288
+ default: links(["About", `caelo:page/${PAGE}`], ["Gone", `caelo:page/${GONE}`]),
289
+ },
290
+ ],
261
291
  },
262
292
  ],
263
293
  },
@@ -279,23 +309,16 @@ describe("composer — nav-menu sets and pre-rendered modules", () => {
279
309
  ],
280
310
  },
281
311
  ],
282
- structuredSets: {
283
- byKindSlug: {
284
- "nav-menu/main": [
285
- { label: "About", href: `caelo:page/${PAGE}` },
286
- { label: "Gone", href: `caelo:page/${GONE}` },
287
- ],
288
- },
289
- },
312
+ lists,
290
313
  linkResolver: index().forPage(PAGE),
291
314
  });
292
- expect(out.html).toContain('<a href="/company/about/">About</a>');
315
+ expect(unstamp(out.html)).toContain('<a href="/company/about/">About</a>');
293
316
  expect(out.html).toContain(`href="caelo:page/${GONE}"`);
294
317
  expect(out.moduleFailures).toEqual([
295
318
  {
296
319
  blockName: "header",
297
320
  moduleId: "m-nav",
298
- moduleSlug: "nav-menu-main",
321
+ moduleSlug: "main-nav",
299
322
  field: "",
300
323
  reason: `link-target-missing:page/${GONE}`,
301
324
  },
@@ -314,7 +337,6 @@ describe("composer — nav-menu sets and pre-rendered modules", () => {
314
337
  templateHtml: '<caelo-slot name="content"></caelo-slot>',
315
338
  templateCss: "",
316
339
  blocks: [],
317
- structuredSets: { byKindSlug: {} },
318
340
  linkResolver: index().forPage(PAGE),
319
341
  });
320
342
  expect(out.html).toContain('<a class="home" href="/company/about/#top">Home</a>');
@@ -631,25 +653,23 @@ describe("page variants (link-resolve) — links go to the rendering page's vari
631
653
  });
632
654
  });
633
655
 
634
- it("covers HTML href, rich text, link field and link-list in one render", () => {
656
+ it("covers HTML href, rich text, link field and list entries in one render", () => {
635
657
  const r = renderTemplate({
636
- html: `<a href="caelo:page/${EN_HOME}">Home</a><a href="{{cta_href}}">Go</a>{{#nav}}<a href="{{href}}">{{label}}</a>{{/nav}}<div>{{body}}</div>`,
658
+ html: `<a href="caelo:page/${EN_HOME}">Home</a><a href="{{cta_href}}">Go</a>{{#nav}}<a href="{{link}}">{{label}}</a>{{/nav}}<div>{{body}}</div>`,
637
659
  fields: [
638
660
  { name: "cta_href", kind: "link" },
639
- { name: "nav", kind: "link-list" },
661
+ { name: "nav", kind: "list", listType: "links" },
640
662
  { name: "body", kind: "richtext" },
641
663
  ],
642
664
  contentValues: {
643
665
  cta_href: `caelo:page/${EN_PRICING}#plans`,
644
- nav: [
645
- { label: "About", href: `caelo:page/${EN_ABOUT}` },
646
- { label: "Solo", href: `caelo:page/${EN_SOLO}` },
647
- ],
666
+ nav: links(["About", `caelo:page/${EN_ABOUT}`], ["Solo", `caelo:page/${EN_SOLO}`]),
648
667
  body: `<p><a href="caelo:page/${EN_ABOUT}?x=1">about</a></p>`,
649
668
  },
669
+ lists,
650
670
  linkResolver: variantIndex("preview").forPage(DE_PRICING),
651
671
  });
652
- expect(r.html).toBe(
672
+ expect(unstamp(r.html)).toBe(
653
673
  '<a href="/de/">Home</a><a href="/de/preise/#plans">Go</a>' +
654
674
  `<a href="/de/ueber/">About</a><a href="caelo:page/${EN_SOLO}">Solo</a>` +
655
675
  '<div><p><a href="/de/ueber/?x=1">about</a></p></div>',
@@ -657,7 +677,7 @@ describe("page variants (link-resolve) — links go to the rendering page's vari
657
677
  expect(r.missingSlots).toEqual([`link-variant-missing:page/${EN_SOLO}:de`]);
658
678
  });
659
679
 
660
- it("covers nav-menu sets in the composer and attributes a missing sibling to the nav", () => {
680
+ it("covers chrome lists in the composer and attributes a missing sibling to the module", () => {
661
681
  const out = composePageWithLayout({
662
682
  layoutHtml:
663
683
  '<html><head></head><body><caelo-slot name="header"></caelo-slot><caelo-slot name="content"></caelo-slot></body></html>',
@@ -668,11 +688,22 @@ describe("page variants (link-resolve) — links go to the rendering page's vari
668
688
  modules: [
669
689
  {
670
690
  moduleId: "m-nav",
671
- slug: "nav-menu-main",
691
+ slug: "main-nav",
672
692
  displayName: "Nav",
673
- html: "",
693
+ html: NAV_HTML,
674
694
  css: "",
675
695
  js: "",
696
+ fields: [
697
+ {
698
+ name: "nav",
699
+ kind: "list",
700
+ listType: "links",
701
+ default: links(
702
+ ["About", `caelo:page/${EN_ABOUT}`],
703
+ ["Solo", `caelo:page/${EN_SOLO}`],
704
+ ),
705
+ },
706
+ ],
676
707
  },
677
708
  ],
678
709
  },
@@ -680,22 +711,15 @@ describe("page variants (link-resolve) — links go to the rendering page's vari
680
711
  templateHtml: '<caelo-slot name="content"></caelo-slot>',
681
712
  templateCss: "",
682
713
  blocks: [],
683
- structuredSets: {
684
- byKindSlug: {
685
- "nav-menu/main": [
686
- { label: "About", href: `caelo:page/${EN_ABOUT}` },
687
- { label: "Solo", href: `caelo:page/${EN_SOLO}` },
688
- ],
689
- },
690
- },
714
+ lists,
691
715
  linkResolver: variantIndex().forPage(DE_ABOUT),
692
716
  });
693
- expect(out.html).toContain('<a href="/de/ueber/">About</a>');
717
+ expect(unstamp(out.html)).toContain('<a href="/de/ueber/">About</a>');
694
718
  expect(out.moduleFailures).toEqual([
695
719
  {
696
720
  blockName: "header",
697
721
  moduleId: "m-nav",
698
- moduleSlug: "nav-menu-main",
722
+ moduleSlug: "main-nav",
699
723
  field: "",
700
724
  reason: `link-variant-missing:page/${EN_SOLO}:de`,
701
725
  },
@@ -745,7 +769,7 @@ describe("link failure markers — what the build gate and the preview report",
745
769
 
746
770
  it("leaves every other marker alone, in order", () => {
747
771
  expect(isLinkFailureMarker("field-not-declared:x")).toBe(false);
748
- expect(isLinkFailureMarker("link-list-malformed:nav[0]")).toBe(false);
772
+ expect(isLinkFailureMarker("list-malformed:nav[items[0]]")).toBe(false);
749
773
  expect(
750
774
  linkMarkersOf([
751
775
  "theme-asset-unbound:logo",
package/src/link-refs.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  * rendered for. Renaming or moving a page then changes no stored link.
10
10
  *
11
11
  * Grammar (one form for every place a link can live — `link` / `url`
12
- * fields, `link-list` items, `*_href` values, module HTML attributes,
13
- * rich text, structured-set items, field defaults):
12
+ * fields, list entry `link` values, `*_href` values, module HTML attributes,
13
+ * rich text, field defaults):
14
14
  *
15
15
  * caelo:page/<pageId>[?query][#anchor]
16
16
  * caelo:media/<mediaId>[/<variant>]
@@ -277,7 +277,7 @@ export function replaceLinkRefs(
277
277
  }
278
278
 
279
279
  /**
280
- * Walk a JSON value (content values, field defaults, set items) and map
280
+ * Walk a JSON value (content values, field defaults, list entries) and map
281
281
  * every string through {@link replaceLinkRefs}. Returns the SAME
282
282
  * reference when nothing changed, so callers can cheaply detect no-ops.
283
283
  */
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Stable value addresses (epic #1107, `docs/lists.md` §9). Core has no
5
+ * locale concept; a translation plugin keys its own storage by these
6
+ * addresses and answers the `list-resolve` composition point with
7
+ * overlays keyed the same way. Entry ids never change, so an address
8
+ * survives moves, renames of loop names and reordering.
9
+ */
10
+
11
+ /** One addressable stored value. */
12
+ export type ValueAddress =
13
+ | {
14
+ readonly kind: "list";
15
+ readonly listId: string;
16
+ readonly itemId: string;
17
+ readonly field: string;
18
+ }
19
+ | {
20
+ readonly kind: "instance";
21
+ readonly contentInstanceId: string;
22
+ readonly field: string;
23
+ readonly itemId: string;
24
+ readonly itemField: string;
25
+ }
26
+ | { readonly kind: "pageProp"; readonly pageId: string; readonly field: string }
27
+ | {
28
+ readonly kind: "listTypeField";
29
+ readonly listTypeId: string;
30
+ readonly node: string;
31
+ readonly field: string;
32
+ };
33
+
34
+ const SEG = "[a-z0-9_-]+";
35
+ const ADDRESS_RE = new RegExp(
36
+ `^(?:list/(${SEG})/item/(${SEG})/(${SEG})` +
37
+ `|instance/(${SEG})/(${SEG})/item/(${SEG})/(${SEG})` +
38
+ `|page/(${SEG})/props/(${SEG})` +
39
+ `|list_type/(${SEG})/node/(${SEG})/(${SEG}))$`,
40
+ );
41
+
42
+ /** Format an address as its canonical string. */
43
+ export function formatValueAddress(a: ValueAddress): string {
44
+ switch (a.kind) {
45
+ case "list":
46
+ return `list/${a.listId}/item/${a.itemId}/${a.field}`;
47
+ case "instance":
48
+ return `instance/${a.contentInstanceId}/${a.field}/item/${a.itemId}/${a.itemField}`;
49
+ case "pageProp":
50
+ return `page/${a.pageId}/props/${a.field}`;
51
+ case "listTypeField":
52
+ return `list_type/${a.listTypeId}/node/${a.node}/${a.field}`;
53
+ }
54
+ }
55
+
56
+ /** Parse an address string; null when it is not one. */
57
+ export function parseValueAddress(s: string): ValueAddress | null {
58
+ const m = ADDRESS_RE.exec(s);
59
+ if (!m) return null;
60
+ if (m[1] && m[2] && m[3]) return { kind: "list", listId: m[1], itemId: m[2], field: m[3] };
61
+ if (m[4] && m[5] && m[6] && m[7]) {
62
+ return {
63
+ kind: "instance",
64
+ contentInstanceId: m[4],
65
+ field: m[5],
66
+ itemId: m[6],
67
+ itemField: m[7],
68
+ };
69
+ }
70
+ if (m[8] && m[9]) return { kind: "pageProp", pageId: m[8], field: m[9] };
71
+ if (m[10] && m[11] && m[12]) {
72
+ return { kind: "listTypeField", listTypeId: m[10], node: m[11], field: m[12] };
73
+ }
74
+ return null;
75
+ }
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The list types every install starts with (epic #1107,
5
+ * `docs/lists.md` §3). They are seeded as STORED data by migration 0352
6
+ * and edited like any other type afterwards — these objects are the
7
+ * source the seed and the tests share, not a render-time fallback
8
+ * (CLAUDE.md §2).
9
+ */
10
+
11
+ import type { ListTypeDef } from "./list-type.js";
12
+
13
+ export const DEFAULT_LIST_TYPES: readonly ListTypeDef[] = [
14
+ {
15
+ slug: "texts",
16
+ name: "Texts",
17
+ purpose: "A flat list of short texts inside one module: feature bullets, chips, steps.",
18
+ role: "content",
19
+ root: { allow: ["text"] },
20
+ nodes: {
21
+ text: {
22
+ label: "Text",
23
+ fields: [{ name: "text", label: "Text", type: "text", required: true }],
24
+ },
25
+ },
26
+ },
27
+ {
28
+ slug: "links",
29
+ name: "Links",
30
+ purpose: "A flat group of links: legal links, social links, a footer column.",
31
+ role: "content",
32
+ root: { allow: ["link"] },
33
+ nodes: {
34
+ link: {
35
+ label: "Link",
36
+ fields: [
37
+ { name: "label", label: "Label", type: "text", required: true },
38
+ { name: "link", label: "Link", type: "link", required: true },
39
+ ],
40
+ current_from: "link",
41
+ },
42
+ },
43
+ },
44
+ {
45
+ slug: "menu",
46
+ name: "Menu",
47
+ purpose:
48
+ "Site navigation with up to three levels: header menu, footer menu, mobile menu. Use a custom type for mega menus with columns or teasers.",
49
+ role: "navigation",
50
+ root: { allow: ["entry"] },
51
+ nodes: {
52
+ entry: {
53
+ label: "Menu entry",
54
+ fields: [
55
+ { name: "label", label: "Label", type: "text", required: true },
56
+ { name: "link", label: "Link", type: "link" },
57
+ ],
58
+ children: { name: "children", allow: ["entry"] },
59
+ current_from: "link",
60
+ },
61
+ },
62
+ max_depth: 3,
63
+ },
64
+ {
65
+ slug: "categories",
66
+ name: "Categories",
67
+ purpose: "A category tree pages are assigned to: blog sections, product groups, doc areas.",
68
+ role: "vocabulary",
69
+ root: { allow: ["category"] },
70
+ nodes: {
71
+ category: {
72
+ label: "Category",
73
+ fields: [
74
+ { name: "name", label: "Name", type: "text", required: true },
75
+ { name: "slug", label: "Slug", type: "text", required: true },
76
+ { name: "description", label: "Description", type: "richtext" },
77
+ { name: "landing_page", label: "Landing page", type: "link" },
78
+ ],
79
+ children: { name: "children", allow: ["category"] },
80
+ current_from: "landing_page",
81
+ },
82
+ },
83
+ max_depth: 6,
84
+ },
85
+ {
86
+ slug: "tags",
87
+ name: "Tags",
88
+ purpose: "Flat keywords pages are tagged with, for filters, tag clouds and related content.",
89
+ role: "vocabulary",
90
+ root: { allow: ["tag"] },
91
+ nodes: {
92
+ tag: {
93
+ label: "Tag",
94
+ fields: [
95
+ { name: "name", label: "Name", type: "text", required: true },
96
+ { name: "slug", label: "Slug", type: "text", required: true },
97
+ { name: "color", label: "Color", type: "text", maxLength: 9 },
98
+ ],
99
+ },
100
+ },
101
+ },
102
+ ];