@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
@@ -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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"}
@@ -1,114 +0,0 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
-
3
- /**
4
- * P6.7.5 — typed schemas for the `structured_sets` table.
5
- *
6
- * One database table holds N kinds of named lists (nav-menus, taxonomies,
7
- * theme tokens, tag lists, footer link blocks, ...). The `kind`
8
- * discriminator drives both the Zod validator at the Query API layer
9
- * and the renderer that turns the items into HTML at preview / deploy.
10
- *
11
- * Adding a new kind: add a Zod schema below, add it to the
12
- * `structuredSetItem` discriminated union, add a renderer in
13
- * `packages/shared/src/structured-set-renderer.ts`. No DB migration
14
- * required — the table already accepts arbitrary `kind` text.
15
- *
16
- * Item shapes are deliberately **structured data**, not HTML strings.
17
- * That's what makes `change_page_slug` able to retarget every menu
18
- * link automatically: walk the JSON, swap `href` matches.
19
- */
20
-
21
- import { z } from "zod";
22
-
23
- /** Recursive nav menu — supports arbitrary submenu nesting. */
24
- export const navMenuItem: z.ZodType<{
25
- label: string;
26
- href: string;
27
- target?: "_self" | "_blank";
28
- children?: { label: string; href: string }[];
29
- /** When set, the renderer delegates this slot to the named ad plugin. */
30
- adSlotId?: string;
31
- }> = z.lazy(() =>
32
- z
33
- .object({
34
- label: z.string().min(1).max(120),
35
- href: z.string().min(1).max(500),
36
- target: z.enum(["_self", "_blank"]).optional(),
37
- children: z.array(navMenuItem).optional(),
38
- adSlotId: z.string().min(1).max(100).optional(),
39
- })
40
- .strict(),
41
- );
42
-
43
- /** Tree-shaped category list. Used by typed-content / tag pages (P12A). */
44
- export const taxonomyItem = z
45
- .object({
46
- slug: z.string().min(1).max(120),
47
- displayName: z.string().min(1).max(200),
48
- parentSlug: z.string().min(1).max(120).optional(),
49
- description: z.string().max(2000).optional(),
50
- })
51
- .strict();
52
-
53
- /** Flat tag — used by post taxonomies, content filtering. */
54
- export const tagItem = z
55
- .object({
56
- slug: z
57
- .string()
58
- .min(1)
59
- .max(120)
60
- .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"),
61
- displayName: z.string().min(1).max(200),
62
- color: z
63
- .string()
64
- .regex(/^#[0-9a-fA-F]{3,8}$/, "hex color")
65
- .optional(),
66
- })
67
- .strict();
68
-
69
- /** Footer "legal" / related-resources block. */
70
- export const linkListItem = z
71
- .object({
72
- label: z.string().min(1).max(200),
73
- href: z.string().min(1).max(500),
74
- description: z.string().max(500).optional(),
75
- })
76
- .strict();
77
-
78
- /** Discriminated kind → items array. The Query API op picks the
79
- * validator at runtime by reading `kind` first.
80
- *
81
- * v0.11.0 (#45) — `theme` is no longer a structured-set kind. The
82
- * theme primitive moved to its own `themes` table with DTCG-shaped
83
- * jsonb tokens; see `themes.ts` + `theme-render.ts`. */
84
- export const structuredSetKind = z.enum(["nav-menu", "taxonomy", "tags", "link-list"]);
85
- export type StructuredSetKind = z.infer<typeof structuredSetKind>;
86
-
87
- /** Validate `items` against the right schema for `kind`. Throws a
88
- * ZodError on mismatch — callers wrap and return `Err(ValidationFailed)`. */
89
- export function validateStructuredSetItems(kind: StructuredSetKind, items: unknown): unknown[] {
90
- if (!Array.isArray(items)) {
91
- throw new z.ZodError([
92
- {
93
- code: "custom",
94
- path: ["items"],
95
- message: "items must be an array",
96
- },
97
- ]);
98
- }
99
- switch (kind) {
100
- case "nav-menu":
101
- return items.map((it) => navMenuItem.parse(it));
102
- case "taxonomy":
103
- return items.map((it) => taxonomyItem.parse(it));
104
- case "tags":
105
- return items.map((it) => tagItem.parse(it));
106
- case "link-list":
107
- return items.map((it) => linkListItem.parse(it));
108
- }
109
- }
110
-
111
- export type NavMenuItem = z.infer<typeof navMenuItem>;
112
- export type TaxonomyItem = z.infer<typeof taxonomyItem>;
113
- export type TagItem = z.infer<typeof tagItem>;
114
- export type LinkListItem = z.infer<typeof linkListItem>;
@@ -1,90 +0,0 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
-
3
- /**
4
- * Plugin data lists in the template engine: `{{#name}}…{{/name}}` over
5
- * data a plugin supplies per page, with the module owning the markup.
6
- *
7
- * The load-bearing case is the third one. Plugin activation is a hard
8
- * state, so a module written while a plugin ran keeps its
9
- * `{{#language_links}}` after the plugin is switched off. Rendering
10
- * that as an empty string would be a silent fallback (CLAUDE.md §2) and
11
- * would read to the operator as "the switcher vanished for no reason".
12
- * It has to stay visible AND say which plugin to turn back on.
13
- */
14
-
15
- import { describe, expect, it } from "bun:test";
16
- import { renderTemplate } from "./template-engine.js";
17
-
18
- const SWITCHER =
19
- '<nav>{{#language_links}}<a href="{{href}}">{{label}}</a>{{/language_links}}</nav>';
20
-
21
- describe("template engine — plugin data lists", () => {
22
- it("iterates a plugin list with the module's own markup", () => {
23
- const r = renderTemplate({
24
- html: SWITCHER,
25
- fields: [],
26
- dataLists: {
27
- language_links: [
28
- { href: "/", label: "English", locale: "en" },
29
- { href: "/de", label: "Deutsch", locale: "de" },
30
- ],
31
- },
32
- });
33
- expect(r.html).toBe('<nav><a href="/">English</a><a href="/de">Deutsch</a></nav>');
34
- expect(r.missingSlots).toEqual([]);
35
- });
36
-
37
- it("renders nothing for an empty list — that is data, not breakage", () => {
38
- // A page with no published translations legitimately has no links.
39
- const r = renderTemplate({
40
- html: SWITCHER,
41
- fields: [],
42
- dataLists: { language_links: [] },
43
- });
44
- expect(r.html).toBe("<nav></nav>");
45
- expect(r.missingSlots).toEqual([]);
46
- });
47
-
48
- it("stays loud and names the plugin when its plugin is switched off", () => {
49
- const r = renderTemplate({
50
- html: SWITCHER,
51
- fields: [],
52
- dormantDataLists: { language_links: "international-site" },
53
- });
54
- // Loud-raw: the section survives in the output rather than
55
- // silently collapsing.
56
- expect(r.html).toContain("{{#language_links}}");
57
- expect(r.missingSlots).toContain(
58
- "plugin-list-unavailable:language_links plugin=international-site",
59
- );
60
- });
61
-
62
- it("keeps an unknown name a plain undeclared field", () => {
63
- // No plugin claims it — this really is a typo, and must not be
64
- // reported as a deactivated plugin.
65
- const r = renderTemplate({ html: "<p>{{#nope}}x{{/nope}}</p>", fields: [] });
66
- expect(r.missingSlots).toContain("field-not-declared:nope");
67
- });
68
-
69
- it("lets a module's own field win over a plugin list of the same name", () => {
70
- // Module fields are resolved first by construction, so a plugin can
71
- // never shadow content the author declared.
72
- const r = renderTemplate({
73
- html: "<ul>{{#items}}<li>{{label}}</li>{{/items}}</ul>",
74
- fields: [{ name: "items", kind: "link-list" }],
75
- contentValues: { items: [{ label: "authored", href: "/a" }] },
76
- dataLists: { items: [{ label: "from-plugin" }] },
77
- });
78
- expect(r.html).toContain("authored");
79
- expect(r.html).not.toContain("from-plugin");
80
- });
81
-
82
- it("leaves a key the items do not carry raw", () => {
83
- const r = renderTemplate({
84
- html: "{{#l}}<a href={{href}}>{{missing_key}}</a>{{/l}}",
85
- fields: [],
86
- dataLists: { l: [{ href: "/x" }] },
87
- });
88
- expect(r.html).toContain("{{missing_key}}");
89
- });
90
- });