@storyblok/schema 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (78) hide show
  1. package/dist/field-plugins/index.cjs +4 -0
  2. package/dist/field-plugins/index.d.cts +2 -0
  3. package/dist/field-plugins/index.d.mts +2 -0
  4. package/dist/field-plugins/index.mjs +3 -0
  5. package/dist/field-plugins/storyblok-color-field.cjs +32 -0
  6. package/dist/field-plugins/storyblok-color-field.cjs.map +1 -0
  7. package/dist/field-plugins/storyblok-color-field.d.cts +16 -0
  8. package/dist/field-plugins/storyblok-color-field.d.mts +16 -0
  9. package/dist/field-plugins/storyblok-color-field.mjs +32 -0
  10. package/dist/field-plugins/storyblok-color-field.mjs.map +1 -0
  11. package/dist/generated/overlay/zod.gen.cjs +0 -2
  12. package/dist/generated/overlay/zod.gen.cjs.map +1 -1
  13. package/dist/generated/overlay/zod.gen.mjs +228 -229
  14. package/dist/generated/overlay/zod.gen.mjs.map +1 -1
  15. package/dist/generated/types/block.d.cts +6 -0
  16. package/dist/generated/types/block.d.mts +6 -0
  17. package/dist/generated/types/field.d.cts +53 -18
  18. package/dist/generated/types/field.d.mts +53 -18
  19. package/dist/generated/types/mapi-story.d.cts +4 -4
  20. package/dist/generated/types/mapi-story.d.mts +4 -4
  21. package/dist/generated/types/story.d.cts +3 -3
  22. package/dist/generated/types/story.d.mts +3 -3
  23. package/dist/helpers/define-block.cjs +6 -1
  24. package/dist/helpers/define-block.cjs.map +1 -1
  25. package/dist/helpers/define-block.d.cts +14 -4
  26. package/dist/helpers/define-block.d.mts +14 -4
  27. package/dist/helpers/define-block.mjs +6 -1
  28. package/dist/helpers/define-block.mjs.map +1 -1
  29. package/dist/helpers/define-field-plugin.cjs +21 -0
  30. package/dist/helpers/define-field-plugin.cjs.map +1 -0
  31. package/dist/helpers/define-field-plugin.d.cts +33 -0
  32. package/dist/helpers/define-field-plugin.d.mts +33 -0
  33. package/dist/helpers/define-field-plugin.mjs +20 -0
  34. package/dist/helpers/define-field-plugin.mjs.map +1 -0
  35. package/dist/helpers/define-field.cjs +8 -1
  36. package/dist/helpers/define-field.cjs.map +1 -1
  37. package/dist/helpers/define-field.d.cts +12 -3
  38. package/dist/helpers/define-field.d.mts +12 -3
  39. package/dist/helpers/define-field.mjs +8 -1
  40. package/dist/helpers/define-field.mjs.map +1 -1
  41. package/dist/helpers/define-folder.cjs +19 -0
  42. package/dist/helpers/define-folder.cjs.map +1 -0
  43. package/dist/helpers/define-folder.d.cts +39 -0
  44. package/dist/helpers/define-folder.d.mts +39 -0
  45. package/dist/helpers/define-folder.mjs +18 -0
  46. package/dist/helpers/define-folder.mjs.map +1 -0
  47. package/dist/helpers/define-schema.cjs +23 -0
  48. package/dist/helpers/define-schema.cjs.map +1 -0
  49. package/dist/helpers/define-schema.d.cts +31 -0
  50. package/dist/helpers/define-schema.d.mts +31 -0
  51. package/dist/helpers/define-schema.mjs +22 -0
  52. package/dist/helpers/define-schema.mjs.map +1 -0
  53. package/dist/helpers/schema-type.d.cts +16 -8
  54. package/dist/helpers/schema-type.d.mts +16 -8
  55. package/dist/index.cjs +6 -0
  56. package/dist/index.d.cts +4 -1
  57. package/dist/index.d.mts +4 -1
  58. package/dist/index.mjs +4 -1
  59. package/dist/utils/slugify-folder-path.cjs +21 -0
  60. package/dist/utils/slugify-folder-path.cjs.map +1 -0
  61. package/dist/utils/slugify-folder-path.mjs +20 -0
  62. package/dist/utils/slugify-folder-path.mjs.map +1 -0
  63. package/dist/validators/internal-schemas.mjs +1 -1
  64. package/dist/validators/shapes.cjs.map +1 -1
  65. package/dist/validators/shapes.d.cts +16 -3
  66. package/dist/validators/shapes.d.mts +16 -3
  67. package/dist/validators/shapes.mjs.map +1 -1
  68. package/dist/validators/validate-schema.cjs +21 -1
  69. package/dist/validators/validate-schema.cjs.map +1 -1
  70. package/dist/validators/validate-schema.d.cts +2 -1
  71. package/dist/validators/validate-schema.d.mts +2 -1
  72. package/dist/validators/validate-schema.mjs +21 -1
  73. package/dist/validators/validate-schema.mjs.map +1 -1
  74. package/dist/validators/validate-story.cjs +96 -32
  75. package/dist/validators/validate-story.cjs.map +1 -1
  76. package/dist/validators/validate-story.mjs +96 -33
  77. package/dist/validators/validate-story.mjs.map +1 -1
  78. package/package.json +13 -2
@@ -1,7 +1,10 @@
1
+ const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
1
2
  const require_is_record = require('../utils/is-record.cjs');
2
3
  const require_shapes = require('./shapes.cjs');
3
4
  const require_zod_gen = require('../generated/overlay/zod.gen.cjs');
4
5
  require('./internal-schemas.cjs');
6
+ const require_slugify_folder_path = require('../utils/slugify-folder-path.cjs');
7
+ let zod = require("zod");
5
8
 
6
9
  //#region src/validators/validate-story.ts
7
10
  /** Field-content keys that are not user-defined fields. */
@@ -10,10 +13,29 @@ const RESERVED_KEYS = new Set([
10
13
  "component",
11
14
  "_editable"
12
15
  ]);
16
+ /**
17
+ * Relaxed plugin envelope used by the `custom` case. Mirrors the generated
18
+ * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching
19
+ * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen
20
+ * regenerate cannot revert it.
21
+ */
22
+ const zPluginEnvelope = zod.z.object({
23
+ plugin: zod.z.string(),
24
+ _uid: zod.z.optional(zod.z.string())
25
+ });
13
26
  /** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */
14
27
  function checkValue(schema, value, path, entity, issues) {
15
28
  const result = schema["~standard"].validate(value);
16
- if (result instanceof Promise) return;
29
+ if (result instanceof Promise) {
30
+ issues.push({
31
+ severity: "error",
32
+ code: "async_validator_unsupported",
33
+ path,
34
+ entity,
35
+ message: "Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it."
36
+ });
37
+ return;
38
+ }
17
39
  if (result.issues) for (const issue of result.issues) {
18
40
  const issuePath = (issue.path ?? []).map((segment) => typeof segment === "object" && segment !== null ? String(segment.key) : segment);
19
41
  issues.push({
@@ -25,7 +47,7 @@ function checkValue(schema, value, path, entity, issues) {
25
47
  });
26
48
  }
27
49
  }
28
- function validateFieldValue(field, value, blocksByName, path, entity, issues) {
50
+ function validateFieldValue(field, value, blocksByName, fieldPluginsByType, path, entity, issues) {
29
51
  switch (field.type) {
30
52
  case "asset":
31
53
  checkValue(require_zod_gen.zAssetFieldValue, value, path, entity, issues);
@@ -46,11 +68,17 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
46
68
  break;
47
69
  case "richtext":
48
70
  checkValue(require_zod_gen.zRichtextFieldValue, value, path, entity, issues);
49
- validateRichtextBloks(value, blocksByName, path, issues);
71
+ validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues);
50
72
  break;
51
- case "custom":
52
- checkValue(require_zod_gen.zPluginFieldValue, value, path, entity, issues);
73
+ case "custom": {
74
+ checkValue(zPluginEnvelope, value, path, entity, issues);
75
+ const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : void 0;
76
+ if (validator && require_is_record.isRecord(value)) {
77
+ const { plugin: _plugin, _uid, ...pluginValue } = value;
78
+ checkValue(validator, pluginValue, path, entity, issues);
79
+ }
53
80
  break;
81
+ }
54
82
  case "bloks":
55
83
  if (!Array.isArray(value)) {
56
84
  pushTypeIssue(value, "array", path, entity, issues);
@@ -58,18 +86,8 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
58
86
  }
59
87
  checkCount(value.length, field.minimum, field.maximum, "block(s)", path, entity, issues);
60
88
  value.forEach((item, index) => {
61
- if (field.allow && field.allow.length > 0 && require_is_record.isRecord(item) && typeof item.component === "string" && !field.allow.includes(item.component)) issues.push({
62
- severity: "error",
63
- code: "disallowed_component",
64
- path: [
65
- ...path,
66
- index,
67
- "component"
68
- ],
69
- entity,
70
- message: `Component "${item.component}" is not allowed in field "${field.name}"; allowed: ${field.allow.join(", ")}.`
71
- });
72
- validateBlokContent(item, blocksByName, [...path, index], issues);
89
+ checkComponentAllowed(field, item, [...path, index], blocksByName, entity, issues);
90
+ validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);
73
91
  });
74
92
  break;
75
93
  case "text":
@@ -115,6 +133,42 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
115
133
  break;
116
134
  }
117
135
  }
136
+ /**
137
+ * Enforces a field's `allow` list for one embedded blok. Shared by the `bloks`
138
+ * case and the richtext walk so both apply the same rule: `mapFieldToWire`
139
+ * pushes folder/name `allow` as an editor/API restriction on *both* field types,
140
+ * so validation must reject the same components the editor and API would.
141
+ *
142
+ * `itemPath` is the path to the blok item (its index); the reported issue points
143
+ * at that item's `component` key. A component is allowed when it is named
144
+ * directly in `allow` or its block sits in (or under) an allowed folder — both
145
+ * sides canonicalized to slug space so a folder referenced two ways (a
146
+ * `defineFolder` ref vs. a string shorthand with different casing/separators)
147
+ * matches the way the CLI/editor group it.
148
+ */
149
+ function checkComponentAllowed(field, item, itemPath, blocksByName, entity, issues) {
150
+ const allowEntries = field.allow ?? [];
151
+ if (allowEntries.length === 0 || !require_is_record.isRecord(item) || typeof item.component !== "string") return;
152
+ const blockNamesAllowed = allowEntries.filter((entry) => typeof entry === "string");
153
+ const folderPathsAllowed = allowEntries.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.folder === "string");
154
+ const itemBlockFolder = blocksByName.get(item.component)?.folder;
155
+ const allowedByName = blockNamesAllowed.includes(item.component);
156
+ const itemFolderSlug = typeof itemBlockFolder === "string" ? require_slugify_folder_path.slugifyFolderPath(itemBlockFolder) : void 0;
157
+ const allowedByFolder = itemFolderSlug !== void 0 && folderPathsAllowed.some(({ folder }) => {
158
+ const allowedSlug = require_slugify_folder_path.slugifyFolderPath(folder);
159
+ return itemFolderSlug === allowedSlug || itemFolderSlug.startsWith(`${allowedSlug}/`);
160
+ });
161
+ if (!allowedByName && !allowedByFolder) {
162
+ const allowedList = allowEntries.map((entry) => typeof entry === "string" ? entry : `folder:${entry.folder}`).join(", ");
163
+ issues.push({
164
+ severity: "error",
165
+ code: "disallowed_component",
166
+ path: [...itemPath, "component"],
167
+ entity,
168
+ message: `Component "${item.component}" is not allowed in field "${field.name}"; allowed: ${allowedList}.`
169
+ });
170
+ }
171
+ }
118
172
  /** Reports a constraint (bound/length/count) violation as an error issue. */
119
173
  function pushConstraint(message, path, entity, issues) {
120
174
  issues.push({
@@ -164,28 +218,37 @@ function pushTypeIssue(value, expected, path, entity, issues) {
164
218
  message: `Expected ${expected}, received ${value === null ? "null" : typeof value}.`
165
219
  });
166
220
  }
167
- /** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */
168
- function validateRichtextBloks(value, blocksByName, path, issues) {
221
+ /**
222
+ * Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`).
223
+ * `field` is the owning richtext field: each embedded blok is checked against its
224
+ * `allow` list ({@link checkComponentAllowed}), matching the group/name
225
+ * restriction `mapFieldToWire` pushes for richtext fields.
226
+ */
227
+ function validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues) {
169
228
  if (!require_is_record.isRecord(value) || !Array.isArray(value.content)) return;
170
229
  value.content.forEach((node, index) => {
171
230
  if (!require_is_record.isRecord(node)) return;
172
- if (node.type === "blok" && require_is_record.isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => validateBlokContent(blok, blocksByName, [
173
- ...path,
174
- "content",
175
- index,
176
- "attrs",
177
- "body",
178
- blokIndex
179
- ], issues));
180
- else if (Array.isArray(node.content)) validateRichtextBloks(node, blocksByName, [
231
+ if (node.type === "blok" && require_is_record.isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => {
232
+ const blokPath = [
233
+ ...path,
234
+ "content",
235
+ index,
236
+ "attrs",
237
+ "body",
238
+ blokIndex
239
+ ];
240
+ checkComponentAllowed(field, blok, blokPath, blocksByName, entity, issues);
241
+ validateBlokContent(blok, blocksByName, fieldPluginsByType, blokPath, issues);
242
+ });
243
+ else if (Array.isArray(node.content)) validateRichtextBloks(node, field, blocksByName, fieldPluginsByType, [
181
244
  ...path,
182
245
  "content",
183
246
  index
184
- ], issues);
247
+ ], entity, issues);
185
248
  });
186
249
  }
187
250
  /** Validates a single blok content object against its component definition. */
188
- function validateBlokContent(content, blocksByName, path, issues) {
251
+ function validateBlokContent(content, blocksByName, fieldPluginsByType, path, issues) {
189
252
  if (!require_is_record.isRecord(content)) {
190
253
  issues.push({
191
254
  severity: "error",
@@ -230,7 +293,7 @@ function validateBlokContent(content, blocksByName, path, issues) {
230
293
  });
231
294
  continue;
232
295
  }
233
- validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);
296
+ validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);
234
297
  }
235
298
  }
236
299
  /**
@@ -245,7 +308,8 @@ function validateBlokContent(content, blocksByName, path, issues) {
245
308
  function validateStory(story, schema) {
246
309
  const issues = [];
247
310
  const blocksByName = new Map(require_shapes.toValues(schema.blocks).map((block) => [block.name, block]));
248
- validateBlokContent(require_is_record.isRecord(story) ? story.content : void 0, blocksByName, ["content"], issues);
311
+ const fieldPluginsByType = new Map(require_shapes.toValues(schema.fieldPlugins).map((plugin) => [plugin.fieldType, plugin.value]));
312
+ validateBlokContent(require_is_record.isRecord(story) ? story.content : void 0, blocksByName, fieldPluginsByType, ["content"], issues);
249
313
  return {
250
314
  ok: issues.every((issue) => issue.severity !== "error"),
251
315
  issues
@@ -1 +1 @@
1
- {"version":3,"file":"validate-story.cjs","names":["zAssetFieldValue","zMultilinkFieldValue","zTableFieldValue","zRichtextFieldValue","zPluginFieldValue","isRecord","toValues"],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zPluginFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // The Zod schemas are synchronous; a thenable result would indicate misuse.\n if (result instanceof Promise) {\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, path, issues);\n break;\n case 'custom':\n checkValue(zPluginFieldValue, value, path, entity, issues);\n break;\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate-story.cjs","names":["z","zAssetFieldValue","zMultilinkFieldValue","zTableFieldValue","zRichtextFieldValue","isRecord","slugifyFolderPath","toValues"],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\nimport { slugifyFolderPath } from '../utils/slugify-folder-path';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/**\n * Relaxed plugin envelope used by the `custom` case. Mirrors the generated\n * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching\n * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen\n * regenerate cannot revert it.\n */\nconst zPluginEnvelope = z.object({ plugin: z.string(), _uid: z.optional(z.string()) });\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // `validateStory` is synchronous. The internal Zod schemas never return a\n // thenable, but a registered field plugin may ship an async validator — which\n // cannot be awaited here. Surface it as an error instead of silently passing,\n // which would report a false `ok: true`.\n if (result instanceof Promise) {\n issues.push({\n severity: 'error',\n code: 'async_validator_unsupported',\n path,\n entity,\n message: 'Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it.',\n });\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues);\n break;\n case 'custom': {\n checkValue(zPluginEnvelope, value, path, entity, issues);\n const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : undefined;\n if (validator && isRecord(value)) {\n // Envelope keys sit alongside the plugin's own keys; strip them so the\n // plugin validator sees only its value. Sibling keys keep issue paths\n // accurate (an issue at ['color'] maps to [...path, 'color']).\n const { plugin: _plugin, _uid, ...pluginValue } = value;\n checkValue(validator, pluginValue, path, entity, issues);\n }\n break;\n }\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n checkComponentAllowed(field, item, [...path, index], blocksByName, entity, issues);\n validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/**\n * Enforces a field's `allow` list for one embedded blok. Shared by the `bloks`\n * case and the richtext walk so both apply the same rule: `mapFieldToWire`\n * pushes folder/name `allow` as an editor/API restriction on *both* field types,\n * so validation must reject the same components the editor and API would.\n *\n * `itemPath` is the path to the blok item (its index); the reported issue points\n * at that item's `component` key. A component is allowed when it is named\n * directly in `allow` or its block sits in (or under) an allowed folder — both\n * sides canonicalized to slug space so a folder referenced two ways (a\n * `defineFolder` ref vs. a string shorthand with different casing/separators)\n * matches the way the CLI/editor group it.\n */\nfunction checkComponentAllowed(\n field: SchemaFieldLike,\n item: unknown,\n itemPath: (string | number)[],\n blocksByName: Map<string, SchemaBlockLike>,\n entity: string,\n issues: ValidationIssue[],\n): void {\n const allowEntries = field.allow ?? [];\n if (allowEntries.length === 0 || !isRecord(item) || typeof item.component !== 'string') {\n return;\n }\n const blockNamesAllowed = allowEntries.filter((entry): entry is string => typeof entry === 'string');\n const folderPathsAllowed = allowEntries.filter(\n (entry): entry is { folder: string } =>\n typeof entry === 'object' && entry !== null && typeof entry.folder === 'string',\n );\n const itemBlock = blocksByName.get(item.component);\n const itemBlockFolder = itemBlock?.folder;\n const allowedByName = blockNamesAllowed.includes(item.component);\n const itemFolderSlug = typeof itemBlockFolder === 'string' ? slugifyFolderPath(itemBlockFolder) : undefined;\n const allowedByFolder = itemFolderSlug !== undefined\n && folderPathsAllowed.some(({ folder }) => {\n const allowedSlug = slugifyFolderPath(folder);\n return itemFolderSlug === allowedSlug || itemFolderSlug.startsWith(`${allowedSlug}/`);\n });\n if (!allowedByName && !allowedByFolder) {\n const allowedList = allowEntries\n .map(entry => (typeof entry === 'string' ? entry : `folder:${entry.folder}`))\n .join(', ');\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...itemPath, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${allowedList}.`,\n });\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/**\n * Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`).\n * `field` is the owning richtext field: each embedded blok is checked against its\n * `allow` list ({@link checkComponentAllowed}), matching the group/name\n * restriction `mapFieldToWire` pushes for richtext fields.\n */\nfunction validateRichtextBloks(\n value: unknown,\n field: SchemaFieldLike,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) => {\n const blokPath = [...path, 'content', index, 'attrs', 'body', blokIndex];\n checkComponentAllowed(field, blok, blokPath, blocksByName, entity, issues);\n validateBlokContent(blok, blocksByName, fieldPluginsByType, blokPath, issues);\n });\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, field, blocksByName, fieldPluginsByType, [...path, 'content', index], entity, issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const fieldPluginsByType = new Map(\n toValues(schema.fieldPlugins).map(plugin => [plugin.fieldType, plugin.value]),\n );\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, fieldPluginsByType, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
@@ -1,7 +1,9 @@
1
1
  import { isRecord } from "../utils/is-record.mjs";
2
2
  import { toValues } from "./shapes.mjs";
3
- import { zAssetFieldValue, zMultilinkFieldValue, zPluginFieldValue, zRichtextFieldValue, zTableFieldValue } from "../generated/overlay/zod.gen.mjs";
3
+ import { zAssetFieldValue, zMultilinkFieldValue, zRichtextFieldValue, zTableFieldValue } from "../generated/overlay/zod.gen.mjs";
4
4
  import "./internal-schemas.mjs";
5
+ import { slugifyFolderPath } from "../utils/slugify-folder-path.mjs";
6
+ import { z } from "zod";
5
7
 
6
8
  //#region src/validators/validate-story.ts
7
9
  /** Field-content keys that are not user-defined fields. */
@@ -10,10 +12,29 @@ const RESERVED_KEYS = new Set([
10
12
  "component",
11
13
  "_editable"
12
14
  ]);
15
+ /**
16
+ * Relaxed plugin envelope used by the `custom` case. Mirrors the generated
17
+ * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching
18
+ * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen
19
+ * regenerate cannot revert it.
20
+ */
21
+ const zPluginEnvelope = z.object({
22
+ plugin: z.string(),
23
+ _uid: z.optional(z.string())
24
+ });
13
25
  /** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */
14
26
  function checkValue(schema, value, path, entity, issues) {
15
27
  const result = schema["~standard"].validate(value);
16
- if (result instanceof Promise) return;
28
+ if (result instanceof Promise) {
29
+ issues.push({
30
+ severity: "error",
31
+ code: "async_validator_unsupported",
32
+ path,
33
+ entity,
34
+ message: "Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it."
35
+ });
36
+ return;
37
+ }
17
38
  if (result.issues) for (const issue of result.issues) {
18
39
  const issuePath = (issue.path ?? []).map((segment) => typeof segment === "object" && segment !== null ? String(segment.key) : segment);
19
40
  issues.push({
@@ -25,7 +46,7 @@ function checkValue(schema, value, path, entity, issues) {
25
46
  });
26
47
  }
27
48
  }
28
- function validateFieldValue(field, value, blocksByName, path, entity, issues) {
49
+ function validateFieldValue(field, value, blocksByName, fieldPluginsByType, path, entity, issues) {
29
50
  switch (field.type) {
30
51
  case "asset":
31
52
  checkValue(zAssetFieldValue, value, path, entity, issues);
@@ -46,11 +67,17 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
46
67
  break;
47
68
  case "richtext":
48
69
  checkValue(zRichtextFieldValue, value, path, entity, issues);
49
- validateRichtextBloks(value, blocksByName, path, issues);
70
+ validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues);
50
71
  break;
51
- case "custom":
52
- checkValue(zPluginFieldValue, value, path, entity, issues);
72
+ case "custom": {
73
+ checkValue(zPluginEnvelope, value, path, entity, issues);
74
+ const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : void 0;
75
+ if (validator && isRecord(value)) {
76
+ const { plugin: _plugin, _uid, ...pluginValue } = value;
77
+ checkValue(validator, pluginValue, path, entity, issues);
78
+ }
53
79
  break;
80
+ }
54
81
  case "bloks":
55
82
  if (!Array.isArray(value)) {
56
83
  pushTypeIssue(value, "array", path, entity, issues);
@@ -58,18 +85,8 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
58
85
  }
59
86
  checkCount(value.length, field.minimum, field.maximum, "block(s)", path, entity, issues);
60
87
  value.forEach((item, index) => {
61
- if (field.allow && field.allow.length > 0 && isRecord(item) && typeof item.component === "string" && !field.allow.includes(item.component)) issues.push({
62
- severity: "error",
63
- code: "disallowed_component",
64
- path: [
65
- ...path,
66
- index,
67
- "component"
68
- ],
69
- entity,
70
- message: `Component "${item.component}" is not allowed in field "${field.name}"; allowed: ${field.allow.join(", ")}.`
71
- });
72
- validateBlokContent(item, blocksByName, [...path, index], issues);
88
+ checkComponentAllowed(field, item, [...path, index], blocksByName, entity, issues);
89
+ validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);
73
90
  });
74
91
  break;
75
92
  case "text":
@@ -115,6 +132,42 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
115
132
  break;
116
133
  }
117
134
  }
135
+ /**
136
+ * Enforces a field's `allow` list for one embedded blok. Shared by the `bloks`
137
+ * case and the richtext walk so both apply the same rule: `mapFieldToWire`
138
+ * pushes folder/name `allow` as an editor/API restriction on *both* field types,
139
+ * so validation must reject the same components the editor and API would.
140
+ *
141
+ * `itemPath` is the path to the blok item (its index); the reported issue points
142
+ * at that item's `component` key. A component is allowed when it is named
143
+ * directly in `allow` or its block sits in (or under) an allowed folder — both
144
+ * sides canonicalized to slug space so a folder referenced two ways (a
145
+ * `defineFolder` ref vs. a string shorthand with different casing/separators)
146
+ * matches the way the CLI/editor group it.
147
+ */
148
+ function checkComponentAllowed(field, item, itemPath, blocksByName, entity, issues) {
149
+ const allowEntries = field.allow ?? [];
150
+ if (allowEntries.length === 0 || !isRecord(item) || typeof item.component !== "string") return;
151
+ const blockNamesAllowed = allowEntries.filter((entry) => typeof entry === "string");
152
+ const folderPathsAllowed = allowEntries.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.folder === "string");
153
+ const itemBlockFolder = blocksByName.get(item.component)?.folder;
154
+ const allowedByName = blockNamesAllowed.includes(item.component);
155
+ const itemFolderSlug = typeof itemBlockFolder === "string" ? slugifyFolderPath(itemBlockFolder) : void 0;
156
+ const allowedByFolder = itemFolderSlug !== void 0 && folderPathsAllowed.some(({ folder }) => {
157
+ const allowedSlug = slugifyFolderPath(folder);
158
+ return itemFolderSlug === allowedSlug || itemFolderSlug.startsWith(`${allowedSlug}/`);
159
+ });
160
+ if (!allowedByName && !allowedByFolder) {
161
+ const allowedList = allowEntries.map((entry) => typeof entry === "string" ? entry : `folder:${entry.folder}`).join(", ");
162
+ issues.push({
163
+ severity: "error",
164
+ code: "disallowed_component",
165
+ path: [...itemPath, "component"],
166
+ entity,
167
+ message: `Component "${item.component}" is not allowed in field "${field.name}"; allowed: ${allowedList}.`
168
+ });
169
+ }
170
+ }
118
171
  /** Reports a constraint (bound/length/count) violation as an error issue. */
119
172
  function pushConstraint(message, path, entity, issues) {
120
173
  issues.push({
@@ -164,28 +217,37 @@ function pushTypeIssue(value, expected, path, entity, issues) {
164
217
  message: `Expected ${expected}, received ${value === null ? "null" : typeof value}.`
165
218
  });
166
219
  }
167
- /** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */
168
- function validateRichtextBloks(value, blocksByName, path, issues) {
220
+ /**
221
+ * Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`).
222
+ * `field` is the owning richtext field: each embedded blok is checked against its
223
+ * `allow` list ({@link checkComponentAllowed}), matching the group/name
224
+ * restriction `mapFieldToWire` pushes for richtext fields.
225
+ */
226
+ function validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues) {
169
227
  if (!isRecord(value) || !Array.isArray(value.content)) return;
170
228
  value.content.forEach((node, index) => {
171
229
  if (!isRecord(node)) return;
172
- if (node.type === "blok" && isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => validateBlokContent(blok, blocksByName, [
173
- ...path,
174
- "content",
175
- index,
176
- "attrs",
177
- "body",
178
- blokIndex
179
- ], issues));
180
- else if (Array.isArray(node.content)) validateRichtextBloks(node, blocksByName, [
230
+ if (node.type === "blok" && isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => {
231
+ const blokPath = [
232
+ ...path,
233
+ "content",
234
+ index,
235
+ "attrs",
236
+ "body",
237
+ blokIndex
238
+ ];
239
+ checkComponentAllowed(field, blok, blokPath, blocksByName, entity, issues);
240
+ validateBlokContent(blok, blocksByName, fieldPluginsByType, blokPath, issues);
241
+ });
242
+ else if (Array.isArray(node.content)) validateRichtextBloks(node, field, blocksByName, fieldPluginsByType, [
181
243
  ...path,
182
244
  "content",
183
245
  index
184
- ], issues);
246
+ ], entity, issues);
185
247
  });
186
248
  }
187
249
  /** Validates a single blok content object against its component definition. */
188
- function validateBlokContent(content, blocksByName, path, issues) {
250
+ function validateBlokContent(content, blocksByName, fieldPluginsByType, path, issues) {
189
251
  if (!isRecord(content)) {
190
252
  issues.push({
191
253
  severity: "error",
@@ -230,7 +292,7 @@ function validateBlokContent(content, blocksByName, path, issues) {
230
292
  });
231
293
  continue;
232
294
  }
233
- validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);
295
+ validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);
234
296
  }
235
297
  }
236
298
  /**
@@ -245,7 +307,8 @@ function validateBlokContent(content, blocksByName, path, issues) {
245
307
  function validateStory(story, schema) {
246
308
  const issues = [];
247
309
  const blocksByName = new Map(toValues(schema.blocks).map((block) => [block.name, block]));
248
- validateBlokContent(isRecord(story) ? story.content : void 0, blocksByName, ["content"], issues);
310
+ const fieldPluginsByType = new Map(toValues(schema.fieldPlugins).map((plugin) => [plugin.fieldType, plugin.value]));
311
+ validateBlokContent(isRecord(story) ? story.content : void 0, blocksByName, fieldPluginsByType, ["content"], issues);
249
312
  return {
250
313
  ok: issues.every((issue) => issue.severity !== "error"),
251
314
  issues
@@ -1 +1 @@
1
- {"version":3,"file":"validate-story.mjs","names":[],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zPluginFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // The Zod schemas are synchronous; a thenable result would indicate misuse.\n if (result instanceof Promise) {\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, path, issues);\n break;\n case 'custom':\n checkValue(zPluginFieldValue, value, path, entity, issues);\n break;\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate-story.mjs","names":[],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\nimport { slugifyFolderPath } from '../utils/slugify-folder-path';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/**\n * Relaxed plugin envelope used by the `custom` case. Mirrors the generated\n * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching\n * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen\n * regenerate cannot revert it.\n */\nconst zPluginEnvelope = z.object({ plugin: z.string(), _uid: z.optional(z.string()) });\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // `validateStory` is synchronous. The internal Zod schemas never return a\n // thenable, but a registered field plugin may ship an async validator — which\n // cannot be awaited here. Surface it as an error instead of silently passing,\n // which would report a false `ok: true`.\n if (result instanceof Promise) {\n issues.push({\n severity: 'error',\n code: 'async_validator_unsupported',\n path,\n entity,\n message: 'Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it.',\n });\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, field, blocksByName, fieldPluginsByType, path, entity, issues);\n break;\n case 'custom': {\n checkValue(zPluginEnvelope, value, path, entity, issues);\n const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : undefined;\n if (validator && isRecord(value)) {\n // Envelope keys sit alongside the plugin's own keys; strip them so the\n // plugin validator sees only its value. Sibling keys keep issue paths\n // accurate (an issue at ['color'] maps to [...path, 'color']).\n const { plugin: _plugin, _uid, ...pluginValue } = value;\n checkValue(validator, pluginValue, path, entity, issues);\n }\n break;\n }\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n checkComponentAllowed(field, item, [...path, index], blocksByName, entity, issues);\n validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/**\n * Enforces a field's `allow` list for one embedded blok. Shared by the `bloks`\n * case and the richtext walk so both apply the same rule: `mapFieldToWire`\n * pushes folder/name `allow` as an editor/API restriction on *both* field types,\n * so validation must reject the same components the editor and API would.\n *\n * `itemPath` is the path to the blok item (its index); the reported issue points\n * at that item's `component` key. A component is allowed when it is named\n * directly in `allow` or its block sits in (or under) an allowed folder — both\n * sides canonicalized to slug space so a folder referenced two ways (a\n * `defineFolder` ref vs. a string shorthand with different casing/separators)\n * matches the way the CLI/editor group it.\n */\nfunction checkComponentAllowed(\n field: SchemaFieldLike,\n item: unknown,\n itemPath: (string | number)[],\n blocksByName: Map<string, SchemaBlockLike>,\n entity: string,\n issues: ValidationIssue[],\n): void {\n const allowEntries = field.allow ?? [];\n if (allowEntries.length === 0 || !isRecord(item) || typeof item.component !== 'string') {\n return;\n }\n const blockNamesAllowed = allowEntries.filter((entry): entry is string => typeof entry === 'string');\n const folderPathsAllowed = allowEntries.filter(\n (entry): entry is { folder: string } =>\n typeof entry === 'object' && entry !== null && typeof entry.folder === 'string',\n );\n const itemBlock = blocksByName.get(item.component);\n const itemBlockFolder = itemBlock?.folder;\n const allowedByName = blockNamesAllowed.includes(item.component);\n const itemFolderSlug = typeof itemBlockFolder === 'string' ? slugifyFolderPath(itemBlockFolder) : undefined;\n const allowedByFolder = itemFolderSlug !== undefined\n && folderPathsAllowed.some(({ folder }) => {\n const allowedSlug = slugifyFolderPath(folder);\n return itemFolderSlug === allowedSlug || itemFolderSlug.startsWith(`${allowedSlug}/`);\n });\n if (!allowedByName && !allowedByFolder) {\n const allowedList = allowEntries\n .map(entry => (typeof entry === 'string' ? entry : `folder:${entry.folder}`))\n .join(', ');\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...itemPath, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${allowedList}.`,\n });\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/**\n * Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`).\n * `field` is the owning richtext field: each embedded blok is checked against its\n * `allow` list ({@link checkComponentAllowed}), matching the group/name\n * restriction `mapFieldToWire` pushes for richtext fields.\n */\nfunction validateRichtextBloks(\n value: unknown,\n field: SchemaFieldLike,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) => {\n const blokPath = [...path, 'content', index, 'attrs', 'body', blokIndex];\n checkComponentAllowed(field, blok, blokPath, blocksByName, entity, issues);\n validateBlokContent(blok, blocksByName, fieldPluginsByType, blokPath, issues);\n });\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, field, blocksByName, fieldPluginsByType, [...path, 'content', index], entity, issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const fieldPluginsByType = new Map(\n toValues(schema.fieldPlugins).map(plugin => [plugin.fieldType, plugin.value]),\n );\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, fieldPluginsByType, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":";;;;;;;;;AAcA,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAQ;CAAa;CAAY,CAAC;;;;;;;AAQjE,MAAM,kBAAkB,EAAE,OAAO;CAAE,QAAQ,EAAE,QAAQ;CAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;CAAE,CAAC;;AAGtF,SAAS,WACP,QACA,OACA,MACA,QACA,QACM;CACN,MAAM,SAAS,OAAO,aAAa,SAAS,MAAM;AAKlD,KAAI,kBAAkB,SAAS;AAC7B,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN;GACA;GACA,SAAS;GACV,CAAC;AACF;;AAEF,KAAI,OAAO,OACT,MAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,aAAa,MAAM,QAAQ,EAAE,EAAE,KAAI,YACtC,OAAO,YAAY,YAAY,YAAY,OAAO,OAAO,QAAQ,IAAI,GAAI,QAC3E;AACD,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN,MAAM,CAAC,GAAG,MAAM,GAAG,UAAU;GAC7B;GACA,SAAS,MAAM;GAChB,CAAC;;;AAKR,SAAS,mBACP,OACA,OACA,cACA,oBACA,MACA,QACA,QACM;AACN,SAAQ,MAAM,MAAd;EACE,KAAK;AACH,cAAW,kBAAkB,OAAO,MAAM,QAAQ,OAAO;AACzD;EACF,KAAK;AACH,OAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,kBAAc,OAAO,SAAS,MAAM,QAAQ,OAAO;AACnD;;AAEF,SAAM,SAAS,MAAM,UAAU,WAAW,kBAAkB,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,QAAQ,OAAO,CAAC;AACpG,cAAW,MAAM,QAAQ,MAAM,iBAAiB,MAAM,iBAAiB,YAAY,MAAM,QAAQ,OAAO;AACxG;EACF,KAAK;AACH,cAAW,sBAAsB,OAAO,MAAM,QAAQ,OAAO;AAC7D;EACF,KAAK;AACH,cAAW,kBAAkB,OAAO,MAAM,QAAQ,OAAO;AACzD;EACF,KAAK;AACH,cAAW,qBAAqB,OAAO,MAAM,QAAQ,OAAO;AAC5D,yBAAsB,OAAO,OAAO,cAAc,oBAAoB,MAAM,QAAQ,OAAO;AAC3F;EACF,KAAK,UAAU;AACb,cAAW,iBAAiB,OAAO,MAAM,QAAQ,OAAO;GACxD,MAAM,YAAY,MAAM,aAAa,mBAAmB,IAAI,MAAM,WAAW,GAAG;AAChF,OAAI,aAAa,SAAS,MAAM,EAAE;IAIhC,MAAM,EAAE,QAAQ,SAAS,MAAM,GAAG,gBAAgB;AAClD,eAAW,WAAW,aAAa,MAAM,QAAQ,OAAO;;AAE1D;;EAEF,KAAK;AACH,OAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,kBAAc,OAAO,SAAS,MAAM,QAAQ,OAAO;AACnD;;AAEF,cAAW,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,YAAY,MAAM,QAAQ,OAAO;AACxF,SAAM,SAAS,MAAM,UAAU;AAC7B,0BAAsB,OAAO,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,cAAc,QAAQ,OAAO;AAClF,wBAAoB,MAAM,cAAc,oBAAoB,CAAC,GAAG,MAAM,MAAM,EAAE,OAAO;KACrF;AACF;EACF,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,UAAU;AAC7B,kBAAc,OAAO,UAAU,MAAM,QAAQ,OAAO;AACpD;;AAEF,qBAAkB,OAAO,OAAO,MAAM,QAAQ,OAAO;AACrD;EACF,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SACnB,eAAc,OAAO,UAAU,MAAM,QAAQ,OAAO;AAEtD;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UAAU;AAC7B,kBAAc,OAAO,UAAU,MAAM,QAAQ,OAAO;AACpD;;AAEF,OAAI,MAAM,aAAa,QAAQ,QAAQ,MAAM,UAC3C,gBAAe,SAAS,MAAM,2BAA2B,MAAM,UAAU,IAAI,MAAM,QAAQ,OAAO;AAEpG,OAAI,MAAM,aAAa,QAAQ,QAAQ,MAAM,UAC3C,gBAAe,SAAS,MAAM,0BAA0B,MAAM,UAAU,IAAI,MAAM,QAAQ,OAAO;AAEnG,OAAI,MAAM,YAAY,QAAQ,cAAc,MAAM,GAAG,MAAM,SACzD,gBAAe,SAAS,MAAM,iBAAiB,MAAM,SAAS,qBAAqB,MAAM,QAAQ,OAAO;AAE1G,OAAI,MAAM,SAAS,QAAQ,MAAM,QAAQ,KAAK,CAAC,aAAa,OAAO,MAAM,OAAO,MAAM,aAAa,EAAE,EAAE;IACrG,MAAM,OAAO,MAAM,aAAa;AAChC,mBACE,SAAS,MAAM,iCAAiC,MAAM,QAAQ,OAAO,iBAAiB,KAAK,KAAK,GAAG,IACnG,MACA,QACA,OACD;;AAEH;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UACnB,eAAc,OAAO,WAAW,MAAM,QAAQ,OAAO;AAEvD;EACF,KAAK;AACH,OAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAK,SAAQ,OAAO,SAAS,SAAS,EAAE;AACzE,kBAAc,OAAO,YAAY,MAAM,QAAQ,OAAO;AACtD;;AAEF,cAAW,MAAM,QAAQ,QAAQ,MAAM,YAAY,EAAE,QAAQ,MAAM,YAAY,EAAE,aAAa,MAAM,QAAQ,OAAO;AACnH;EACF,KAAK;EACL,KAAK,MAEH;EACF;AAGE,SAAM;AACN;;;;;;;;;;;;;;;;AAiBN,SAAS,sBACP,OACA,MACA,UACA,cACA,QACA,QACM;CACN,MAAM,eAAe,MAAM,SAAS,EAAE;AACtC,KAAI,aAAa,WAAW,KAAK,CAAC,SAAS,KAAK,IAAI,OAAO,KAAK,cAAc,SAC5E;CAEF,MAAM,oBAAoB,aAAa,QAAQ,UAA2B,OAAO,UAAU,SAAS;CACpG,MAAM,qBAAqB,aAAa,QACrC,UACC,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAO,MAAM,WAAW,SAC1E;CAED,MAAM,kBADY,aAAa,IAAI,KAAK,UAAU,EACf;CACnC,MAAM,gBAAgB,kBAAkB,SAAS,KAAK,UAAU;CAChE,MAAM,iBAAiB,OAAO,oBAAoB,WAAW,kBAAkB,gBAAgB,GAAG;CAClG,MAAM,kBAAkB,mBAAmB,UACtC,mBAAmB,MAAM,EAAE,aAAa;EACzC,MAAM,cAAc,kBAAkB,OAAO;AAC7C,SAAO,mBAAmB,eAAe,eAAe,WAAW,GAAG,YAAY,GAAG;GACrF;AACJ,KAAI,CAAC,iBAAiB,CAAC,iBAAiB;EACtC,MAAM,cAAc,aACjB,KAAI,UAAU,OAAO,UAAU,WAAW,QAAQ,UAAU,MAAM,SAAU,CAC5E,KAAK,KAAK;AACb,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN,MAAM,CAAC,GAAG,UAAU,YAAY;GAChC;GACA,SAAS,cAAc,KAAK,UAAU,6BAA6B,MAAM,KAAK,cAAc,YAAY;GACzG,CAAC;;;;AAKN,SAAS,eACP,SACA,MACA,QACA,QACM;AACN,QAAO,KAAK;EAAE,UAAU;EAAS,MAAM;EAAwB;EAAM;EAAQ;EAAS,CAAC;;;AAIzF,SAAS,WACP,QACA,KACA,KACA,MACA,MACA,QACA,QACM;AACN,KAAI,OAAO,QAAQ,SAAS,IAC1B,gBAAe,qBAAqB,IAAI,GAAG,KAAK,aAAa,OAAO,IAAI,MAAM,QAAQ,OAAO;AAE/F,KAAI,OAAO,QAAQ,SAAS,IAC1B,gBAAe,oBAAoB,IAAI,GAAG,KAAK,aAAa,OAAO,IAAI,MAAM,QAAQ,OAAO;;;AAKhG,SAAS,kBACP,OACA,OACA,MACA,QACA,QACM;CACN,MAAM,MAAM,MAAM,cAAc,MAAM;AACtC,KAAI,OAAO,QAAQ,MAAM,SAAS,IAChC,gBAAe,eAAe,MAAM,OAAO,0BAA0B,IAAI,IAAI,MAAM,QAAQ,OAAO;AAEpG,KAAI,MAAM,aAAa,QAAQ,MAAM,SAAS,MAAM,UAClD,gBAAe,eAAe,MAAM,OAAO,2BAA2B,MAAM,UAAU,IAAI,MAAM,QAAQ,OAAO;;;AAKnH,SAAS,cAAc,OAAuB;AAC5C,KAAI,CAAC,OAAO,SAAS,MAAM,CACzB,QAAO;CAGT,MAAM,CAAC,UAAU,YADJ,OAAO,MAAM,CAAC,aAAa,CACN,MAAM,IAAI;CAC5C,MAAM,iBAAiB,SAAS,SAAS,IAAI,GAAG,SAAS,MAAM,IAAI,CAAC,GAAG,SAAS;AAEhF,QAAO,WAAW,KAAK,IAAI,GAAG,iBAAiB,OAAO,SAAS,CAAC,GAAG;;;AAIrE,SAAS,aAAa,OAAe,MAAc,MAAuB;CACxE,MAAM,SAAS,QAAQ,QAAQ;CAC/B,MAAM,YAAY,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,CAAC;AACrD,QAAO,KAAK,IAAI,QAAQ,KAAK,MAAM,MAAM,CAAC,IAAI;;;AAIhD,SAAS,QAAQ,OAA+C;AAC9D,KAAI,SAAS,QAAQ,UAAU,GAC7B;CAEF,MAAM,SAAS,OAAO,MAAM;AAC5B,QAAO,OAAO,SAAS,OAAO,GAAG,SAAS;;AAG5C,SAAS,cACP,OACA,UACA,MACA,QACA,QACM;AACN,QAAO,KAAK;EACV,UAAU;EACV,MAAM;EACN;EACA;EACA,SAAS,YAAY,SAAS,aAAa,UAAU,OAAO,SAAS,OAAO,MAAM;EACnF,CAAC;;;;;;;;AASJ,SAAS,sBACP,OACA,OACA,cACA,oBACA,MACA,QACA,QACM;AACN,KAAI,CAAC,SAAS,MAAM,IAAI,CAAC,MAAM,QAAQ,MAAM,QAAQ,CACnD;AAEF,OAAM,QAAQ,SAAS,MAAM,UAAU;AACrC,MAAI,CAAC,SAAS,KAAK,CACjB;AAEF,MAAI,KAAK,SAAS,UAAU,SAAS,KAAK,MAAM,IAAI,MAAM,QAAQ,KAAK,MAAM,KAAK,CAChF,MAAK,MAAM,KAAK,SAAS,MAAM,cAAc;GAC3C,MAAM,WAAW;IAAC,GAAG;IAAM;IAAW;IAAO;IAAS;IAAQ;IAAU;AACxE,yBAAsB,OAAO,MAAM,UAAU,cAAc,QAAQ,OAAO;AAC1E,uBAAoB,MAAM,cAAc,oBAAoB,UAAU,OAAO;IAC7E;WAEK,MAAM,QAAQ,KAAK,QAAQ,CAElC,uBAAsB,MAAM,OAAO,cAAc,oBAAoB;GAAC,GAAG;GAAM;GAAW;GAAM,EAAE,QAAQ,OAAO;GAEnH;;;AAIJ,SAAS,oBACP,SACA,cACA,oBACA,MACA,QACM;AACN,KAAI,CAAC,SAAS,QAAQ,EAAE;AACtB,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN;GACA,QAAQ;GACR,SAAS;GACV,CAAC;AACF;;CAGF,MAAM,YAAY,QAAQ;CAC1B,MAAM,QAAQ,OAAO,cAAc,WAAW,aAAa,IAAI,UAAU,GAAG;AAC5E,KAAI,CAAC,OAAO;AACV,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN,MAAM,CAAC,GAAG,MAAM,YAAY;GAC5B,QAAQ;GACR,SAAS,sBAAsB,OAAO,UAAU,CAAC;GAClD,CAAC;AACF;;CAGF,MAAM,SAAS,SAAS,MAAM;CAC9B,MAAM,SAAS,MAAM,UAAU,EAAE;CACjC,MAAM,eAAe,IAAI,IAAI,OAAO,KAAI,UAAS,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC;AAEtE,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,KAAI,CAAC,cAAc,IAAI,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CACnD,QAAO,KAAK;EACV,UAAU;EACV,MAAM;EACN,MAAM,CAAC,GAAG,MAAM,IAAI;EACpB;EACA,SAAS,kBAAkB,IAAI,kBAAkB,MAAM,KAAK;EAC7D,CAAC;AAIN,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,QAAQ,MAAM;AAC5B,MAAI,UAAU,UAAa,UAAU,MAAM;AACzC,OAAI,MAAM,SACR,QAAO,KAAK;IACV,UAAU;IACV,MAAM;IACN,MAAM,CAAC,GAAG,MAAM,MAAM,KAAK;IAC3B;IACA,SAAS,2BAA2B,MAAM,KAAK,kBAAkB,MAAM,KAAK;IAC7E,CAAC;AAEJ;;AAEF,qBAAmB,OAAO,OAAO,cAAc,oBAAoB,CAAC,GAAG,MAAM,MAAM,KAAK,EAAE,QAAQ,OAAO;;;;;;;;;;;;AAa7G,SAAgB,cAAc,OAAgB,QAAsC;CAClF,MAAM,SAA4B,EAAE;CACpC,MAAM,eAAe,IAAI,IAAI,SAAS,OAAO,OAAO,CAAC,KAAI,UAAS,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC;CACvF,MAAM,qBAAqB,IAAI,IAC7B,SAAS,OAAO,aAAa,CAAC,KAAI,WAAU,CAAC,OAAO,WAAW,OAAO,MAAM,CAAC,CAC9E;AAED,qBADgB,SAAS,MAAM,GAAG,MAAM,UAAU,QACrB,cAAc,oBAAoB,CAAC,UAAU,EAAE,OAAO;AACnF,QAAO;EAAE,IAAI,OAAO,OAAM,UAAS,MAAM,aAAa,QAAQ;EAAE;EAAQ"}