@kitsra/kavio-schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1491 @@
1
+ export const schemaVersion = "0.1";
2
+ export function extensionForFormat(format) {
3
+ return format === "png-sequence" ? "zip" : format;
4
+ }
5
+ export const supportedMigrationVersions = [schemaVersion];
6
+ export function migrateComposition01To01(document) {
7
+ return document;
8
+ }
9
+ export const schemaMigrations = {
10
+ "0.1": {
11
+ "0.1": migrateComposition01To01
12
+ }
13
+ };
14
+ export function migrateComposition(document, options = {}) {
15
+ const fromVersion = options.fromVersion ?? document.version;
16
+ const toVersion = options.toVersion ?? schemaVersion;
17
+ const migration = schemaMigrations[fromVersion]?.[toVersion];
18
+ if (migration === undefined) {
19
+ throw new RangeError(`Unsupported Kavio schema migration from ${fromVersion} to ${toVersion}.`);
20
+ }
21
+ return migration(document);
22
+ }
23
+ const ASSET_TYPES = new Set(["video", "image", "audio", "font"]);
24
+ const LAYER_TYPES = new Set(["video", "image", "text", "shape", "caption"]);
25
+ const PROP_TYPES = new Set(["string", "number", "boolean", "color", "url", "enum", "asset"]);
26
+ const EXPORT_FORMATS = new Set(["mp4", "webm", "mov", "gif", "png-sequence"]);
27
+ const FIT_VALUES = new Set(["cover", "contain", "fill", "none"]);
28
+ const TEXT_ALIGN_VALUES = new Set(["left", "center", "right"]);
29
+ const TEXT_MOTION_TYPES = new Set([
30
+ "typeOn",
31
+ "cascade",
32
+ "scramble",
33
+ "highlightSweep",
34
+ "trackingIn"
35
+ ]);
36
+ const TEXT_SPLIT_MODES = new Set(["none", "word", "char", "line"]);
37
+ const TEXT_MOTION_ORIGINS = new Set(["start", "center", "end"]);
38
+ const MASK_SOURCE_KINDS = new Set(["shape", "asset", "procedural"]);
39
+ const MASK_SHAPES = new Set(["rect", "circle", "diamond"]);
40
+ const MASK_ASSET_MODES = new Set(["alpha"]);
41
+ const PROCEDURAL_MASK_TYPES = new Set(["linearGradient", "radialGradient", "scanlines"]);
42
+ const MASK_DIRECTIONS = new Set(["up", "down", "left", "right"]);
43
+ const TRANSITION_TYPES = new Set([
44
+ "fade",
45
+ "slide",
46
+ "wipe",
47
+ "crossfade",
48
+ "zoom",
49
+ "push",
50
+ "spin",
51
+ "rotate",
52
+ "flip",
53
+ "blurDissolve",
54
+ "colorDissolve",
55
+ "dip",
56
+ "iris",
57
+ "stretch",
58
+ "squeeze",
59
+ "clockWipe",
60
+ "barWipe",
61
+ "gridWipe",
62
+ "tileReveal",
63
+ "radialBlur",
64
+ "zoomBlur",
65
+ "bookFlip",
66
+ "pageCurlLite",
67
+ "skewSlide",
68
+ "expandMask",
69
+ "letterboxReveal",
70
+ "filmFlash",
71
+ "cameraWhip"
72
+ ]);
73
+ const TRANSITION_DIRECTIONS = new Set(["up", "down", "left", "right"]);
74
+ const TRANSITION_AXES = new Set(["x", "y"]);
75
+ const TRANSITION_SHAPES = new Set(["circle", "diamond"]);
76
+ const ANCHOR_VALUES = new Set([
77
+ "top-left",
78
+ "top",
79
+ "top-right",
80
+ "left",
81
+ "center",
82
+ "right",
83
+ "bottom-left",
84
+ "bottom",
85
+ "bottom-right"
86
+ ]);
87
+ const ANIMATABLE_PROPERTIES = new Set(["opacity", "x", "y", "scale", "rotation"]);
88
+ const EASING_VALUES = new Set([
89
+ "linear",
90
+ "inQuad",
91
+ "outQuad",
92
+ "inOutQuad",
93
+ "inCubic",
94
+ "outCubic",
95
+ "inOutCubic",
96
+ "inCirc",
97
+ "outCirc",
98
+ "inOutCirc",
99
+ "inExpo",
100
+ "outExpo",
101
+ "inOutExpo",
102
+ "anticipate",
103
+ "back",
104
+ "inBack",
105
+ "outBack",
106
+ "inOutBack",
107
+ "inElastic",
108
+ "outElastic",
109
+ "inOutElastic",
110
+ "inBounce",
111
+ "outBounce",
112
+ "inOutBounce"
113
+ ]);
114
+ const TIMING_TYPES = new Set(["tween", "spring", "steps", "sequence", "stagger"]);
115
+ const AUDIO_ROLES = new Set(["music", "voiceover", "sfx", "source"]);
116
+ const CAPTION_SOURCE_KINDS = new Set(["inline", "vtt", "srt", "asset"]);
117
+ const WEB_SAFE_FONT_FAMILIES = new Set([
118
+ "arial",
119
+ "helvetica",
120
+ "times new roman",
121
+ "georgia",
122
+ "courier new",
123
+ "verdana",
124
+ "system-ui",
125
+ "sans-serif",
126
+ "serif",
127
+ "monospace",
128
+ "inter"
129
+ ]);
130
+ const PLACEHOLDER_PATTERN = /{{\s*([^{}]+?)\s*}}/g;
131
+ const PROP_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
132
+ const CUBIC_BEZIER_PATTERN = /^cubic-bezier\(\s*(-?(?:\d+|\d*\.\d+))\s*,\s*(-?(?:\d+|\d*\.\d+))\s*,\s*(-?(?:\d+|\d*\.\d+))\s*,\s*(-?(?:\d+|\d*\.\d+))\s*\)$/;
133
+ export function validateComposition(input) {
134
+ const errors = [];
135
+ if (!isRecord(input)) {
136
+ return {
137
+ ok: false,
138
+ errors: [validationError("SCHEMA_DOCUMENT_TYPE", "", "Composition must be an object.")]
139
+ };
140
+ }
141
+ validateTopLevelShape(input, errors);
142
+ const props = validateProps(input.props, errors);
143
+ const composition = validateCompositionTiming(input.composition, errors);
144
+ const assets = validateAssets(input.assets, errors);
145
+ const layers = validateLayers(input.layers, assets, composition, errors);
146
+ validateTracks(input.tracks, layers, composition, errors);
147
+ validateAudio(input.audio, assets, composition, errors);
148
+ validateExports(input.exports, layers, composition, errors);
149
+ validateInterpolations(input, props, errors);
150
+ return { ok: errors.every((error) => error.severity !== "error"), errors };
151
+ }
152
+ function validateTopLevelShape(input, errors) {
153
+ if (typeof input.version !== "string") {
154
+ errors.push(validationError("SCHEMA_VERSION_REQUIRED", "version", "version is required."));
155
+ }
156
+ else if (input.version !== schemaVersion) {
157
+ errors.push(validationError("SCHEMA_VERSION_UNSUPPORTED", "version", `version must be ${schemaVersion}.`, `Set version to "${schemaVersion}" for this schema package.`));
158
+ }
159
+ if (!isRecord(input.composition)) {
160
+ errors.push(validationError("SCHEMA_COMPOSITION_REQUIRED", "composition", "composition is required."));
161
+ }
162
+ if (input.props !== undefined && !isRecord(input.props)) {
163
+ errors.push(validationError("SCHEMA_INVALID_FIELD", "props", "props must be an object."));
164
+ }
165
+ if (!isRecord(input.assets)) {
166
+ errors.push(validationError("SCHEMA_ASSETS_REQUIRED", "assets", "assets must be an object."));
167
+ }
168
+ if (!Array.isArray(input.layers)) {
169
+ errors.push(validationError("SCHEMA_LAYERS_REQUIRED", "layers", "layers must be an array."));
170
+ }
171
+ if (input.tracks !== undefined && !Array.isArray(input.tracks)) {
172
+ errors.push(validationError("SCHEMA_INVALID_FIELD", "tracks", "tracks must be an array."));
173
+ }
174
+ if (input.audio !== undefined && !Array.isArray(input.audio)) {
175
+ errors.push(validationError("SCHEMA_INVALID_FIELD", "audio", "audio must be an array."));
176
+ }
177
+ if (!Array.isArray(input.exports)) {
178
+ errors.push(validationError("SCHEMA_EXPORTS_REQUIRED", "exports", "exports must be an array."));
179
+ }
180
+ }
181
+ function validateCompositionTiming(value, errors) {
182
+ if (!isRecord(value)) {
183
+ return {};
184
+ }
185
+ const info = {};
186
+ const width = requireInteger(value, "width", "composition.width", 1, 7680, errors);
187
+ const height = requireInteger(value, "height", "composition.height", 1, 7680, errors);
188
+ const fps = requireInteger(value, "fps", "composition.fps", 1, 120, errors);
189
+ const durationFrames = requireInteger(value, "durationFrames", "composition.durationFrames", 1, undefined, errors);
190
+ if (width !== undefined && height !== undefined && width * height > 7680 * 7680) {
191
+ errors.push(validationError("LIMIT_DIMENSIONS_EXCEEDED", "composition", "composition dimensions exceed the maximum supported canvas area.", "Use width and height values up to 7680 pixels."));
192
+ }
193
+ if (fps !== undefined) {
194
+ info.fps = fps;
195
+ if (![24, 25, 30, 50, 60].includes(fps)) {
196
+ errors.push(validationWarning("SCHEMA_FPS_NONSTANDARD", "composition.fps", "fps is valid but non-standard.", "Use 24, 25, 30, 50, or 60 unless this template intentionally needs another frame rate."));
197
+ }
198
+ }
199
+ if (durationFrames !== undefined) {
200
+ info.durationFrames = durationFrames;
201
+ }
202
+ optionalString(value, "background", "composition.background", errors);
203
+ optionalEnum(value, "colorSpace", "composition.colorSpace", new Set(["srgb", "display-p3"]), errors);
204
+ return info;
205
+ }
206
+ function validateProps(value, errors) {
207
+ const props = new Map();
208
+ if (value === undefined || !isRecord(value)) {
209
+ return props;
210
+ }
211
+ for (const [name, declaration] of Object.entries(value)) {
212
+ const path = propertyPath("props", name);
213
+ if (!PROP_NAME_PATTERN.test(name)) {
214
+ errors.push(validationError("PROP_INVALID_NAME", path, "prop names must start with a letter or underscore and contain only letters, numbers, underscores, or hyphens."));
215
+ }
216
+ if (!isRecord(declaration)) {
217
+ errors.push(validationError("PROP_INVALID_DECLARATION", path, "prop declaration must be an object."));
218
+ continue;
219
+ }
220
+ const typeValue = declaration.type;
221
+ if (typeof typeValue !== "string") {
222
+ errors.push(validationError("PROP_REQUIRED_FIELD", propertyPath(path, "type"), "prop type is required."));
223
+ continue;
224
+ }
225
+ if (!isPropType(typeValue)) {
226
+ errors.push(validationError("PROP_INVALID_TYPE", propertyPath(path, "type"), `prop type "${typeValue}" is not supported.`, "Use string, number, boolean, color, url, enum, or asset."));
227
+ continue;
228
+ }
229
+ const requiredValue = declaration.required;
230
+ if (requiredValue !== undefined && typeof requiredValue !== "boolean") {
231
+ errors.push(validationError("PROP_TYPE_MISMATCH", propertyPath(path, "required"), "prop required must be a boolean."));
232
+ }
233
+ let options;
234
+ if (typeValue === "enum") {
235
+ if (!Array.isArray(declaration.options) || declaration.options.length === 0) {
236
+ errors.push(validationError("PROP_REQUIRED_FIELD", propertyPath(path, "options"), "enum props must declare at least one option."));
237
+ }
238
+ else {
239
+ options = declaration.options;
240
+ }
241
+ }
242
+ if (declaration.default !== undefined) {
243
+ validatePropValue(declaration.default, typeValue, propertyPath(path, "default"), options, errors);
244
+ }
245
+ if (declaration.maxLength !== undefined) {
246
+ optionalInteger(declaration, "maxLength", propertyPath(path, "maxLength"), 1, undefined, errors);
247
+ }
248
+ const prop = {
249
+ name,
250
+ path,
251
+ type: typeValue,
252
+ hasDefault: declaration.default !== undefined,
253
+ required: requiredValue === true
254
+ };
255
+ if (options !== undefined) {
256
+ prop.options = options;
257
+ }
258
+ props.set(name, prop);
259
+ }
260
+ return props;
261
+ }
262
+ function validateAssets(value, errors) {
263
+ const assets = new Map();
264
+ if (!isRecord(value)) {
265
+ return assets;
266
+ }
267
+ for (const [id, asset] of Object.entries(value)) {
268
+ const path = propertyPath("assets", id);
269
+ if (!id) {
270
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "asset id must not be empty."));
271
+ }
272
+ if (!isRecord(asset)) {
273
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "asset must be an object."));
274
+ continue;
275
+ }
276
+ const typeValue = requireString(asset, "type", propertyPath(path, "type"), errors);
277
+ requireString(asset, "src", propertyPath(path, "src"), errors);
278
+ if (typeValue === undefined || !isAssetType(typeValue)) {
279
+ if (typeValue !== undefined) {
280
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), `asset type "${typeValue}" is not supported.`, "Use video, image, audio, or font."));
281
+ }
282
+ continue;
283
+ }
284
+ const info = { id, path, type: typeValue };
285
+ if (typeValue === "font") {
286
+ const family = requireString(asset, "family", propertyPath(path, "family"), errors);
287
+ if (family !== undefined) {
288
+ info.family = family;
289
+ }
290
+ }
291
+ optionalInteger(asset, "trimStartFrames", propertyPath(path, "trimStartFrames"), 0, undefined, errors);
292
+ optionalInteger(asset, "trimEndFrames", propertyPath(path, "trimEndFrames"), 0, undefined, errors, true);
293
+ optionalBoolean(asset, "loop", propertyPath(path, "loop"), errors);
294
+ optionalString(asset, "checksum", propertyPath(path, "checksum"), errors);
295
+ assets.set(id, info);
296
+ }
297
+ return assets;
298
+ }
299
+ function validateLayers(value, assets, composition, errors) {
300
+ const layers = new Map();
301
+ const seenLayerIds = new Map();
302
+ if (!Array.isArray(value)) {
303
+ return layers;
304
+ }
305
+ value.forEach((layer, index) => {
306
+ const path = indexPath("layers", index);
307
+ if (!isRecord(layer)) {
308
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "layer must be an object."));
309
+ return;
310
+ }
311
+ const id = requireString(layer, "id", propertyPath(path, "id"), errors);
312
+ const typeValue = requireString(layer, "type", propertyPath(path, "type"), errors);
313
+ const startFrame = requireInteger(layer, "startFrame", propertyPath(path, "startFrame"), 0, undefined, errors);
314
+ const durationFrames = requireInteger(layer, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
315
+ if (id !== undefined) {
316
+ if (id.length === 0) {
317
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "id"), "layer id must not be empty."));
318
+ }
319
+ const firstPath = seenLayerIds.get(id);
320
+ if (firstPath !== undefined) {
321
+ errors.push(validationError("SCHEMA_DUPLICATE_LAYER_ID", propertyPath(path, "id"), `duplicate layer id "${id}".`, `Layer ids must be unique. First seen at ${firstPath}.`));
322
+ }
323
+ else {
324
+ seenLayerIds.set(id, propertyPath(path, "id"));
325
+ }
326
+ }
327
+ const layerInfo = id !== undefined ? { id, path } : undefined;
328
+ if (typeValue !== undefined) {
329
+ if (isLayerType(typeValue)) {
330
+ if (layerInfo !== undefined) {
331
+ layerInfo.type = typeValue;
332
+ }
333
+ validateLayerByType(layer, typeValue, path, assets, errors);
334
+ }
335
+ else {
336
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), `layer type "${typeValue}" is not supported.`, "Use video, image, text, shape, or caption."));
337
+ }
338
+ }
339
+ if (layerInfo !== undefined) {
340
+ layers.set(layerInfo.id, layerInfo);
341
+ }
342
+ validateFrameRange(path, startFrame, durationFrames, composition.durationFrames, errors);
343
+ validateCommonLayerFields(layer, path, assets, errors);
344
+ });
345
+ return layers;
346
+ }
347
+ function validateLayerByType(layer, type, path, assets, errors) {
348
+ switch (type) {
349
+ case "video":
350
+ validateAssetReference(layer.asset, propertyPath(path, "asset"), "video", assets, errors);
351
+ optionalEnum(layer, "fit", propertyPath(path, "fit"), FIT_VALUES, errors);
352
+ validateVideoCrop(layer.crop, propertyPath(path, "crop"), layer.durationFrames, errors);
353
+ optionalBoolean(layer, "muted", propertyPath(path, "muted"), errors);
354
+ optionalNumber(layer, "volume", propertyPath(path, "volume"), 0, 1, errors);
355
+ optionalNumber(layer, "playbackRate", propertyPath(path, "playbackRate"), 0, undefined, errors);
356
+ break;
357
+ case "image":
358
+ validateAssetReference(layer.asset, propertyPath(path, "asset"), "image", assets, errors);
359
+ optionalEnum(layer, "fit", propertyPath(path, "fit"), FIT_VALUES, errors);
360
+ break;
361
+ case "text":
362
+ requireString(layer, "text", propertyPath(path, "text"), errors);
363
+ validateTextStyle(layer.style, propertyPath(path, "style"), assets, errors);
364
+ validateTextMotion(layer.textMotion, propertyPath(path, "textMotion"), errors);
365
+ break;
366
+ case "shape":
367
+ optionalEnum(layer, "shape", propertyPath(path, "shape"), new Set(["rect"]), errors);
368
+ optionalString(layer, "fill", propertyPath(path, "fill"), errors);
369
+ validateStroke(layer.stroke, propertyPath(path, "stroke"), errors);
370
+ optionalNumber(layer, "radius", propertyPath(path, "radius"), 0, undefined, errors);
371
+ break;
372
+ case "caption":
373
+ validateCaptionSource(layer.source, propertyPath(path, "source"), assets, errors);
374
+ validateTextStyle(layer.style, propertyPath(path, "style"), assets, errors);
375
+ break;
376
+ }
377
+ }
378
+ function validateCommonLayerFields(layer, path, assets, errors) {
379
+ validatePosition(layer.position, propertyPath(path, "position"), errors);
380
+ validateSize(layer.size, propertyPath(path, "size"), errors);
381
+ validateAnchor(layer.anchor, propertyPath(path, "anchor"), errors);
382
+ optionalNumber(layer, "opacity", propertyPath(path, "opacity"), 0, 1, errors);
383
+ optionalNumber(layer, "rotation", propertyPath(path, "rotation"), undefined, undefined, errors);
384
+ optionalNumber(layer, "scale", propertyPath(path, "scale"), 0, undefined, errors);
385
+ optionalInteger(layer, "z", propertyPath(path, "z"), undefined, undefined, errors, true);
386
+ optionalString(layer, "track", propertyPath(path, "track"), errors);
387
+ validateKeyframes(layer.keyframes, propertyPath(path, "keyframes"), layer.durationFrames, errors);
388
+ validateLayerMask(layer.mask, propertyPath(path, "mask"), assets, errors);
389
+ if (Array.isArray(layer.effects)) {
390
+ layer.effects.forEach((effect, index) => {
391
+ const effectPath = indexPath(propertyPath(path, "effects"), index);
392
+ if (!isRecord(effect)) {
393
+ errors.push(validationError("SCHEMA_INVALID_FIELD", effectPath, "effect must be an object."));
394
+ return;
395
+ }
396
+ optionalEnum(effect, "type", propertyPath(effectPath, "type"), new Set(["blur", "brightness", "contrast", "saturate", "tint"]), errors);
397
+ });
398
+ }
399
+ else if (layer.effects !== undefined) {
400
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "effects"), "effects must be an array."));
401
+ }
402
+ validateTransition(layer.transitionIn, propertyPath(path, "transitionIn"), errors);
403
+ validateTransition(layer.transitionOut, propertyPath(path, "transitionOut"), errors);
404
+ }
405
+ function validateLayerMask(value, path, assets, errors) {
406
+ if (value === undefined || value === null) {
407
+ return;
408
+ }
409
+ if (!isRecord(value)) {
410
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "mask must be an object or null."));
411
+ return;
412
+ }
413
+ optionalNumber(value, "opacity", propertyPath(path, "opacity"), 0, 1, errors);
414
+ optionalBoolean(value, "invert", propertyPath(path, "invert"), errors);
415
+ if (!isRecord(value.source)) {
416
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", propertyPath(path, "source"), "mask source is required."));
417
+ return;
418
+ }
419
+ validateMaskSource(value.source, propertyPath(path, "source"), assets, errors);
420
+ if (value.invert === true && value.source.kind === "asset") {
421
+ errors.push(validationError("SCHEMA_UNSUPPORTED_MASK_SOURCE", propertyPath(path, "invert"), "inverted asset masks are not supported by the stable browser renderer.", "Use a pre-inverted image asset or omit invert."));
422
+ }
423
+ }
424
+ function validateMaskSource(source, path, assets, errors) {
425
+ const kind = requireString(source, "kind", propertyPath(path, "kind"), errors);
426
+ if (kind !== undefined && !MASK_SOURCE_KINDS.has(kind)) {
427
+ errors.push(validationError("SCHEMA_UNSUPPORTED_MASK_SOURCE", propertyPath(path, "kind"), `mask source kind "${kind}" is not supported.`, "Use shape, asset, or procedural for the current mask model."));
428
+ return;
429
+ }
430
+ validateMaskResolution(source.resolution, propertyPath(path, "resolution"), errors);
431
+ if (kind === "shape") {
432
+ optionalEnum(source, "shape", propertyPath(path, "shape"), MASK_SHAPES, errors);
433
+ if (source.shape === undefined) {
434
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", propertyPath(path, "shape"), "shape masks require shape."));
435
+ }
436
+ return;
437
+ }
438
+ if (kind === "asset") {
439
+ validateAssetReference(source.asset, propertyPath(path, "asset"), "image", assets, errors);
440
+ optionalEnum(source, "mode", propertyPath(path, "mode"), MASK_ASSET_MODES, errors);
441
+ return;
442
+ }
443
+ if (kind === "procedural") {
444
+ const type = requireString(source, "type", propertyPath(path, "type"), errors);
445
+ if (type !== undefined && !PROCEDURAL_MASK_TYPES.has(type)) {
446
+ errors.push(validationError("SCHEMA_UNSUPPORTED_MASK_SOURCE", propertyPath(path, "type"), `procedural mask type "${type}" is not supported.`, "Use linearGradient, radialGradient, or scanlines until heavier fields are implemented."));
447
+ }
448
+ requireInteger(source, "seed", propertyPath(path, "seed"), 0, undefined, errors);
449
+ optionalEnum(source, "direction", propertyPath(path, "direction"), MASK_DIRECTIONS, errors);
450
+ optionalNumber(source, "softness", propertyPath(path, "softness"), 0, 1, errors);
451
+ optionalNumber(source, "frequency", propertyPath(path, "frequency"), 1, 256, errors);
452
+ }
453
+ }
454
+ function validateMaskResolution(value, path, errors) {
455
+ if (value === undefined) {
456
+ return;
457
+ }
458
+ if (!isRecord(value)) {
459
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "mask resolution must be an object."));
460
+ return;
461
+ }
462
+ requireInteger(value, "width", propertyPath(path, "width"), 1, 4096, errors);
463
+ requireInteger(value, "height", propertyPath(path, "height"), 1, 4096, errors);
464
+ }
465
+ function validateTextStyle(value, path, assets, errors) {
466
+ if (value === undefined) {
467
+ return;
468
+ }
469
+ if (!isRecord(value)) {
470
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "style must be an object."));
471
+ return;
472
+ }
473
+ optionalString(value, "fontFamily", propertyPath(path, "fontFamily"), errors);
474
+ optionalNumber(value, "fontSize", propertyPath(path, "fontSize"), 1, 2000, errors);
475
+ optionalNumberOrString(value, "fontWeight", propertyPath(path, "fontWeight"), 1, 1000, errors);
476
+ optionalString(value, "fontStyle", propertyPath(path, "fontStyle"), errors);
477
+ optionalString(value, "color", propertyPath(path, "color"), errors);
478
+ optionalEnum(value, "align", propertyPath(path, "align"), TEXT_ALIGN_VALUES, errors);
479
+ optionalNumber(value, "lineHeight", propertyPath(path, "lineHeight"), 0, undefined, errors);
480
+ optionalNumber(value, "letterSpacing", propertyPath(path, "letterSpacing"), undefined, undefined, errors);
481
+ optionalInteger(value, "maxLines", propertyPath(path, "maxLines"), 0, undefined, errors);
482
+ optionalInteger(value, "maxCharsPerLine", propertyPath(path, "maxCharsPerLine"), 1, undefined, errors);
483
+ optionalBoolean(value, "wrap", propertyPath(path, "wrap"), errors);
484
+ optionalString(value, "background", propertyPath(path, "background"), errors, true);
485
+ optionalNumber(value, "padding", propertyPath(path, "padding"), 0, undefined, errors);
486
+ validateStroke(value.stroke, propertyPath(path, "stroke"), errors);
487
+ if (isRecord(value.shadow)) {
488
+ optionalString(value.shadow, "color", propertyPath(propertyPath(path, "shadow"), "color"), errors);
489
+ optionalNumber(value.shadow, "x", propertyPath(propertyPath(path, "shadow"), "x"), undefined, undefined, errors);
490
+ optionalNumber(value.shadow, "y", propertyPath(propertyPath(path, "shadow"), "y"), undefined, undefined, errors);
491
+ optionalNumber(value.shadow, "blur", propertyPath(propertyPath(path, "shadow"), "blur"), 0, undefined, errors);
492
+ }
493
+ else if (value.shadow !== undefined && value.shadow !== null) {
494
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "shadow"), "shadow must be an object or null."));
495
+ }
496
+ if (isRecord(value.highlight)) {
497
+ optionalEnum(value.highlight, "mode", propertyPath(propertyPath(path, "highlight"), "mode"), new Set(["none", "word", "line"]), errors);
498
+ optionalString(value.highlight, "color", propertyPath(propertyPath(path, "highlight"), "color"), errors);
499
+ optionalNumber(value.highlight, "scale", propertyPath(propertyPath(path, "highlight"), "scale"), 0, undefined, errors);
500
+ }
501
+ else if (value.highlight !== undefined) {
502
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "highlight"), "highlight must be an object."));
503
+ }
504
+ const fontFamily = value.fontFamily;
505
+ if (typeof fontFamily === "string") {
506
+ validateRegisteredFont(fontFamily, propertyPath(path, "fontFamily"), assets, errors);
507
+ }
508
+ }
509
+ function validateTextMotion(value, path, errors) {
510
+ if (value === undefined) {
511
+ return;
512
+ }
513
+ if (!isRecord(value)) {
514
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "textMotion must be an object."));
515
+ return;
516
+ }
517
+ const type = requireString(value, "type", propertyPath(path, "type"), errors);
518
+ if (type !== undefined && !TEXT_MOTION_TYPES.has(type)) {
519
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), `textMotion type "${type}" is not supported.`, "Use typeOn, cascade, scramble, highlightSweep, or trackingIn."));
520
+ }
521
+ optionalEnum(value, "split", propertyPath(path, "split"), TEXT_SPLIT_MODES, errors);
522
+ optionalInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
523
+ validateEasing(value.easing, propertyPath(path, "easing"), errors);
524
+ optionalInteger(value, "staggerFrames", propertyPath(path, "staggerFrames"), 0, undefined, errors);
525
+ optionalEnum(value, "origin", propertyPath(path, "origin"), TEXT_MOTION_ORIGINS, errors);
526
+ optionalInteger(value, "seed", propertyPath(path, "seed"), undefined, undefined, errors);
527
+ optionalBoolean(value, "preserveLayout", propertyPath(path, "preserveLayout"), errors);
528
+ optionalEnum(value, "direction", propertyPath(path, "direction"), TRANSITION_DIRECTIONS, errors);
529
+ optionalNumber(value, "amount", propertyPath(path, "amount"), 0, undefined, errors);
530
+ optionalNumber(value, "intensity", propertyPath(path, "intensity"), 0, undefined, errors);
531
+ optionalString(value, "color", propertyPath(path, "color"), errors);
532
+ validateTextRestingBox(value.restingBox, propertyPath(path, "restingBox"), errors);
533
+ }
534
+ function validateTextRestingBox(value, path, errors) {
535
+ if (value === undefined) {
536
+ return;
537
+ }
538
+ if (!isRecord(value)) {
539
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "restingBox must be an object."));
540
+ return;
541
+ }
542
+ optionalNumber(value, "width", propertyPath(path, "width"), 0, undefined, errors);
543
+ optionalNumber(value, "height", propertyPath(path, "height"), 0, undefined, errors);
544
+ }
545
+ function validateStroke(value, path, errors) {
546
+ if (value === undefined || value === null) {
547
+ return;
548
+ }
549
+ if (!isRecord(value)) {
550
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "stroke must be an object or null."));
551
+ return;
552
+ }
553
+ optionalString(value, "color", propertyPath(path, "color"), errors);
554
+ optionalNumber(value, "width", propertyPath(path, "width"), 0, undefined, errors);
555
+ }
556
+ function validateCaptionSource(value, path, assets, errors) {
557
+ if (!isRecord(value)) {
558
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, "caption source is required."));
559
+ return;
560
+ }
561
+ const kind = requireString(value, "kind", propertyPath(path, "kind"), errors);
562
+ if (kind !== undefined && !CAPTION_SOURCE_KINDS.has(kind)) {
563
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "kind"), `caption source kind "${kind}" is not supported.`, "Use inline, vtt, srt, or asset."));
564
+ }
565
+ if (kind === "inline") {
566
+ if (!Array.isArray(value.cues)) {
567
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", propertyPath(path, "cues"), "inline captions require cues."));
568
+ }
569
+ else {
570
+ validateCaptionCues(value.cues, propertyPath(path, "cues"), errors);
571
+ }
572
+ }
573
+ else if (kind === "vtt" || kind === "srt" || kind === "asset") {
574
+ validateAssetReference(value.asset, propertyPath(path, "asset"), undefined, assets, errors);
575
+ }
576
+ }
577
+ function validateCaptionCues(cues, path, errors) {
578
+ cues.forEach((cue, index) => {
579
+ const cuePath = indexPath(path, index);
580
+ if (!isRecord(cue)) {
581
+ errors.push(validationError("SCHEMA_INVALID_FIELD", cuePath, "caption cue must be an object."));
582
+ return;
583
+ }
584
+ const startFrame = requireInteger(cue, "startFrame", propertyPath(cuePath, "startFrame"), 0, undefined, errors);
585
+ const endFrame = requireInteger(cue, "endFrame", propertyPath(cuePath, "endFrame"), 0, undefined, errors);
586
+ requireString(cue, "text", propertyPath(cuePath, "text"), errors);
587
+ if (startFrame !== undefined && endFrame !== undefined && endFrame <= startFrame) {
588
+ errors.push(validationError("SCHEMA_FRAME_RANGE_INVALID", cuePath, "caption cue endFrame must be greater than startFrame."));
589
+ }
590
+ if (cue.words !== undefined) {
591
+ if (!Array.isArray(cue.words)) {
592
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(cuePath, "words"), "caption words must be an array."));
593
+ }
594
+ else {
595
+ validateCaptionWords(cue.words, propertyPath(cuePath, "words"), errors);
596
+ }
597
+ }
598
+ });
599
+ }
600
+ function validateCaptionWords(words, path, errors) {
601
+ words.forEach((word, index) => {
602
+ const wordPath = indexPath(path, index);
603
+ if (!isRecord(word)) {
604
+ errors.push(validationError("SCHEMA_INVALID_FIELD", wordPath, "caption word must be an object."));
605
+ return;
606
+ }
607
+ const startFrame = requireInteger(word, "startFrame", propertyPath(wordPath, "startFrame"), 0, undefined, errors);
608
+ const endFrame = requireInteger(word, "endFrame", propertyPath(wordPath, "endFrame"), 0, undefined, errors);
609
+ requireString(word, "text", propertyPath(wordPath, "text"), errors);
610
+ if (startFrame !== undefined && endFrame !== undefined && endFrame <= startFrame) {
611
+ errors.push(validationError("SCHEMA_FRAME_RANGE_INVALID", wordPath, "caption word endFrame must be greater than startFrame."));
612
+ }
613
+ });
614
+ }
615
+ function validateTracks(value, layers, composition, errors) {
616
+ if (value === undefined || !Array.isArray(value)) {
617
+ return;
618
+ }
619
+ const seenTrackIds = new Map();
620
+ value.forEach((track, index) => {
621
+ const path = indexPath("tracks", index);
622
+ if (!isRecord(track)) {
623
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "track must be an object."));
624
+ return;
625
+ }
626
+ const id = requireString(track, "id", propertyPath(path, "id"), errors);
627
+ if (id !== undefined) {
628
+ const firstPath = seenTrackIds.get(id);
629
+ if (firstPath !== undefined) {
630
+ errors.push(validationError("SCHEMA_DUPLICATE_TRACK_ID", propertyPath(path, "id"), `duplicate track id "${id}".`, `Track ids must be unique. First seen at ${firstPath}.`));
631
+ }
632
+ else {
633
+ seenTrackIds.set(id, propertyPath(path, "id"));
634
+ }
635
+ }
636
+ if (!Array.isArray(track.clips)) {
637
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", propertyPath(path, "clips"), "track clips must be an array."));
638
+ return;
639
+ }
640
+ validateTrackClips(track.clips, propertyPath(path, "clips"), layers, composition, errors);
641
+ });
642
+ }
643
+ function validateTrackClips(clips, path, layers, composition, errors) {
644
+ const seenClipIds = new Map();
645
+ const windows = [];
646
+ let previousClip;
647
+ clips.forEach((clip, index) => {
648
+ const clipPath = indexPath(path, index);
649
+ if (!isRecord(clip)) {
650
+ errors.push(validationError("SCHEMA_INVALID_FIELD", clipPath, "track clip must be an object."));
651
+ return;
652
+ }
653
+ const id = requireString(clip, "id", propertyPath(clipPath, "id"), errors);
654
+ const layerId = requireString(clip, "layerId", propertyPath(clipPath, "layerId"), errors);
655
+ const startFrame = requireInteger(clip, "startFrame", propertyPath(clipPath, "startFrame"), 0, undefined, errors);
656
+ const durationFrames = requireInteger(clip, "durationFrames", propertyPath(clipPath, "durationFrames"), 1, undefined, errors);
657
+ if (id !== undefined) {
658
+ const firstPath = seenClipIds.get(id);
659
+ if (firstPath !== undefined) {
660
+ errors.push(validationError("SCHEMA_DUPLICATE_TRACK_CLIP_ID", propertyPath(clipPath, "id"), `duplicate track clip id "${id}".`, `Clip ids must be unique within a track. First seen at ${firstPath}.`));
661
+ }
662
+ else {
663
+ seenClipIds.set(id, propertyPath(clipPath, "id"));
664
+ }
665
+ }
666
+ if (layerId !== undefined && !layers.has(layerId)) {
667
+ errors.push(validationError("SCHEMA_UNKNOWN_LAYER_REFERENCE", propertyPath(clipPath, "layerId"), `track clip references unknown layer "${layerId}".`));
668
+ }
669
+ validateFrameRange(clipPath, startFrame, durationFrames, composition.durationFrames, errors);
670
+ validateTransitionSeriesDefinition(clip.transitionFromPrevious, propertyPath(clipPath, "transitionFromPrevious"), errors);
671
+ const clipInfo = {
672
+ startFrame,
673
+ durationFrames
674
+ };
675
+ if (clip.transitionFromPrevious !== undefined) {
676
+ if (index === 0 || previousClip === undefined) {
677
+ errors.push(validationError("TRANSITION_SERIES_PREVIOUS_REQUIRED", propertyPath(clipPath, "transitionFromPrevious"), "transitionFromPrevious requires a previous clip on the same track."));
678
+ }
679
+ else {
680
+ const duration = transitionSeriesDuration(clip.transitionFromPrevious);
681
+ if (duration !== undefined &&
682
+ previousClip.startFrame !== undefined &&
683
+ previousClip.durationFrames !== undefined &&
684
+ startFrame !== undefined &&
685
+ durationFrames !== undefined) {
686
+ const start = startFrame;
687
+ const end = start + duration;
688
+ const previousEnd = previousClip.startFrame + previousClip.durationFrames;
689
+ const nextEnd = startFrame + durationFrames;
690
+ if (end > previousEnd || end > nextEnd) {
691
+ errors.push(validationError("TRANSITION_SERIES_OVERLAP_INVALID", propertyPath(clipPath, "transitionFromPrevious"), "transitionFromPrevious timing must fit inside the overlap between the previous clip and this clip.", "Set this clip to start before the previous clip ends, or shorten timing.durationFrames."));
692
+ }
693
+ windows.push({ path: propertyPath(clipPath, "transitionFromPrevious"), startFrame: start, endFrame: end });
694
+ }
695
+ }
696
+ }
697
+ previousClip = clipInfo;
698
+ });
699
+ validateTrackTransitionConflicts(windows, errors);
700
+ }
701
+ function validateTrackTransitionConflicts(windows, errors) {
702
+ const sorted = [...windows].sort((left, right) => left.startFrame - right.startFrame || left.endFrame - right.endFrame);
703
+ let previous;
704
+ for (const window of sorted) {
705
+ if (previous !== undefined && window.startFrame < previous.endFrame) {
706
+ errors.push(validationError("TRANSITION_SERIES_CONFLICT", window.path, "transitionFromPrevious overlaps another transition window on the same track.", `Adjust timing so this transition starts at or after frame ${previous.endFrame}.`));
707
+ }
708
+ if (previous === undefined || window.endFrame > previous.endFrame) {
709
+ previous = { path: window.path, endFrame: window.endFrame };
710
+ }
711
+ }
712
+ }
713
+ function validateTransitionSeriesDefinition(value, path, errors) {
714
+ if (value === undefined) {
715
+ return;
716
+ }
717
+ if (!isRecord(value)) {
718
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "transitionFromPrevious must be an object."));
719
+ return;
720
+ }
721
+ validateTransitionPresentation(value.presentation, propertyPath(path, "presentation"), errors);
722
+ validateTransitionTiming(value.timing, propertyPath(path, "timing"), errors);
723
+ }
724
+ function validateTransitionPresentation(value, path, errors) {
725
+ if (!isRecord(value)) {
726
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, "transition presentation is required."));
727
+ return;
728
+ }
729
+ const type = requireString(value, "type", propertyPath(path, "type"), errors);
730
+ if (type !== undefined && !TRANSITION_TYPES.has(type)) {
731
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), "type has an unsupported value."));
732
+ }
733
+ optionalEnum(value, "direction", propertyPath(path, "direction"), TRANSITION_DIRECTIONS, errors);
734
+ optionalEnum(value, "axis", propertyPath(path, "axis"), TRANSITION_AXES, errors);
735
+ optionalEnum(value, "shape", propertyPath(path, "shape"), TRANSITION_SHAPES, errors);
736
+ optionalString(value, "color", propertyPath(path, "color"), errors);
737
+ optionalNumber(value, "amount", propertyPath(path, "amount"), 0, undefined, errors);
738
+ optionalNumber(value, "intensity", propertyPath(path, "intensity"), 0, undefined, errors);
739
+ optionalInteger(value, "rows", propertyPath(path, "rows"), 1, 32, errors);
740
+ optionalInteger(value, "columns", propertyPath(path, "columns"), 1, 32, errors);
741
+ }
742
+ function validateTransitionTiming(value, path, errors) {
743
+ if (!isRecord(value)) {
744
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, "transition timing is required."));
745
+ return;
746
+ }
747
+ const type = requireString(value, "type", propertyPath(path, "type"), errors);
748
+ if (type !== undefined && type !== "tween") {
749
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), "transition timing type has an unsupported value."));
750
+ }
751
+ requireInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
752
+ validateEasing(value.easing, propertyPath(path, "easing"), errors);
753
+ }
754
+ function transitionSeriesDuration(value) {
755
+ if (!isRecord(value) || !isRecord(value.timing)) {
756
+ return undefined;
757
+ }
758
+ const durationFrames = value.timing.durationFrames;
759
+ return isInteger(durationFrames) && durationFrames >= 1 ? durationFrames : undefined;
760
+ }
761
+ function validateAudio(value, assets, composition, errors) {
762
+ if (value === undefined || !Array.isArray(value)) {
763
+ return;
764
+ }
765
+ value.forEach((track, index) => {
766
+ const path = indexPath("audio", index);
767
+ if (!isRecord(track)) {
768
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "audio track must be an object."));
769
+ return;
770
+ }
771
+ optionalString(track, "id", propertyPath(path, "id"), errors);
772
+ validateAssetReference(track.asset, propertyPath(path, "asset"), "audio", assets, errors);
773
+ optionalEnum(track, "role", propertyPath(path, "role"), AUDIO_ROLES, errors);
774
+ const startFrame = optionalInteger(track, "startFrame", propertyPath(path, "startFrame"), 0, undefined, errors);
775
+ const durationFrames = optionalInteger(track, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
776
+ validateFrameRange(path, startFrame, durationFrames, composition.durationFrames, errors);
777
+ optionalInteger(track, "offsetFrames", propertyPath(path, "offsetFrames"), 0, undefined, errors);
778
+ optionalNumber(track, "volume", propertyPath(path, "volume"), 0, 1, errors);
779
+ optionalInteger(track, "fadeInFrames", propertyPath(path, "fadeInFrames"), 0, undefined, errors);
780
+ optionalInteger(track, "fadeOutFrames", propertyPath(path, "fadeOutFrames"), 0, undefined, errors);
781
+ optionalBoolean(track, "loop", propertyPath(path, "loop"), errors);
782
+ });
783
+ }
784
+ function validateExports(value, layers, composition, errors) {
785
+ if (!Array.isArray(value)) {
786
+ return;
787
+ }
788
+ if (value.length === 0) {
789
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", "exports", "exports must contain at least one preset."));
790
+ }
791
+ value.forEach((exportPreset, index) => {
792
+ const path = indexPath("exports", index);
793
+ if (!isRecord(exportPreset)) {
794
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "export preset must be an object."));
795
+ return;
796
+ }
797
+ requireString(exportPreset, "name", propertyPath(path, "name"), errors);
798
+ const format = requireString(exportPreset, "format", propertyPath(path, "format"), errors);
799
+ requireInteger(exportPreset, "width", propertyPath(path, "width"), 1, 7680, errors);
800
+ requireInteger(exportPreset, "height", propertyPath(path, "height"), 1, 7680, errors);
801
+ optionalInteger(exportPreset, "fps", propertyPath(path, "fps"), 1, 120, errors);
802
+ optionalString(exportPreset, "bitrate", propertyPath(path, "bitrate"), errors);
803
+ optionalInteger(exportPreset, "crf", propertyPath(path, "crf"), 0, 63, errors);
804
+ optionalString(exportPreset, "audioCodec", propertyPath(path, "audioCodec"), errors);
805
+ optionalString(exportPreset, "audioBitrate", propertyPath(path, "audioBitrate"), errors);
806
+ optionalNumber(exportPreset, "loudnessLufs", propertyPath(path, "loudnessLufs"), undefined, undefined, errors);
807
+ optionalString(exportPreset, "background", propertyPath(path, "background"), errors, true);
808
+ if (format !== undefined && !isExportFormat(format)) {
809
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "format"), `export format "${format}" is not supported.`, "Use mp4, webm, mov, gif, or png-sequence."));
810
+ }
811
+ validateExportCodec(exportPreset, path, format, errors);
812
+ validateExportBackground(exportPreset, path, format, errors);
813
+ validateLayerOverrides(exportPreset.layerOverrides, propertyPath(path, "layerOverrides"), layers, composition, errors);
814
+ });
815
+ }
816
+ function validateExportCodec(exportPreset, path, format, errors) {
817
+ const codec = exportPreset.codec;
818
+ if (codec === undefined) {
819
+ return;
820
+ }
821
+ if (typeof codec !== "string") {
822
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "codec"), "export codec must be a string."));
823
+ return;
824
+ }
825
+ if (!isExportFormat(format)) {
826
+ return;
827
+ }
828
+ const allowedCodecsByFormat = {
829
+ mp4: ["h264", "hevc"],
830
+ webm: ["vp9"],
831
+ mov: ["prores"],
832
+ gif: [],
833
+ "png-sequence": []
834
+ };
835
+ const allowed = allowedCodecsByFormat[format];
836
+ if (!allowed.includes(codec)) {
837
+ errors.push(validationError("SCHEMA_UNSUPPORTED_EXPORT_CODEC", propertyPath(path, "codec"), `codec "${codec}" is not supported for ${format} exports.`, allowed.length > 0 ? `Use ${allowed.join(" or ")} for ${format}.` : `Remove codec for ${format} exports.`));
838
+ }
839
+ }
840
+ function validateExportBackground(exportPreset, path, format, errors) {
841
+ if (exportPreset.background !== "transparent" || !isExportFormat(format)) {
842
+ return;
843
+ }
844
+ if (format !== "webm" && format !== "mov" && format !== "png-sequence") {
845
+ errors.push(validationError("SCHEMA_UNSUPPORTED_EXPORT_BACKGROUND", propertyPath(path, "background"), `transparent background is not supported for ${format} exports.`, "Use webm, mov, or png-sequence for alpha output."));
846
+ }
847
+ }
848
+ function validateLayerOverrides(value, path, layers, composition, errors) {
849
+ if (value === undefined) {
850
+ return;
851
+ }
852
+ if (!isRecord(value)) {
853
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "layerOverrides must be an object."));
854
+ return;
855
+ }
856
+ for (const [layerId, override] of Object.entries(value)) {
857
+ const overridePath = propertyPath(path, layerId);
858
+ if (!layers.has(layerId)) {
859
+ errors.push(validationError("SCHEMA_UNKNOWN_LAYER_REFERENCE", overridePath, `layer override references unknown layer "${layerId}".`));
860
+ }
861
+ if (!isRecord(override)) {
862
+ errors.push(validationError("SCHEMA_INVALID_FIELD", overridePath, "layer override must be an object."));
863
+ continue;
864
+ }
865
+ const startFrame = optionalInteger(override, "startFrame", propertyPath(overridePath, "startFrame"), 0, undefined, errors);
866
+ const durationFrames = optionalInteger(override, "durationFrames", propertyPath(overridePath, "durationFrames"), 1, undefined, errors);
867
+ validateFrameRange(overridePath, startFrame, durationFrames, composition.durationFrames, errors);
868
+ validatePosition(override.position, propertyPath(overridePath, "position"), errors);
869
+ validateSize(override.size, propertyPath(overridePath, "size"), errors);
870
+ validateAnchor(override.anchor, propertyPath(overridePath, "anchor"), errors);
871
+ validateVideoCrop(override.crop, propertyPath(overridePath, "crop"), durationFrames, errors);
872
+ }
873
+ }
874
+ function validateVideoCrop(value, path, layerDuration, errors) {
875
+ if (value === undefined) {
876
+ return;
877
+ }
878
+ if (!isRecord(value)) {
879
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "crop must be an object."));
880
+ return;
881
+ }
882
+ const mode = requireString(value, "mode", propertyPath(path, "mode"), errors);
883
+ if (mode !== undefined && mode !== "center" && mode !== "subject") {
884
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "mode"), `crop mode "${mode}" is not supported.`, 'Use "center" or "subject".'));
885
+ }
886
+ if (mode === "center") {
887
+ return;
888
+ }
889
+ const x = optionalNumber(value, "x", propertyPath(path, "x"), 0, 1, errors);
890
+ const y = optionalNumber(value, "y", propertyPath(path, "y"), 0, 1, errors);
891
+ optionalInteger(value, "smoothingFrames", propertyPath(path, "smoothingFrames"), 0, undefined, errors);
892
+ optionalString(value, "source", propertyPath(path, "source"), errors);
893
+ if (value.keyframes !== undefined) {
894
+ if (!Array.isArray(value.keyframes)) {
895
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "keyframes"), "crop keyframes must be an array."));
896
+ }
897
+ else {
898
+ validateCropKeyframes(value.keyframes, propertyPath(path, "keyframes"), layerDuration, errors);
899
+ }
900
+ }
901
+ if (mode === "subject" && x === undefined && y === undefined && !Array.isArray(value.keyframes)) {
902
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, "subject crop must declare x/y or at least one keyframe.", "Use normalized subject coordinates from 0 to 1."));
903
+ }
904
+ }
905
+ function validateCropKeyframes(value, path, layerDuration, errors) {
906
+ let previousFrame = -1;
907
+ const duration = isInteger(layerDuration) && layerDuration >= 1 ? layerDuration : undefined;
908
+ value.forEach((keyframe, index) => {
909
+ const keyframePath = indexPath(path, index);
910
+ if (!isRecord(keyframe)) {
911
+ errors.push(validationError("SCHEMA_INVALID_FIELD", keyframePath, "crop keyframe must be an object."));
912
+ return;
913
+ }
914
+ const frame = requireInteger(keyframe, "frame", propertyPath(keyframePath, "frame"), 0, undefined, errors);
915
+ requireNumber(keyframe, "x", propertyPath(keyframePath, "x"), 0, 1, errors);
916
+ requireNumber(keyframe, "y", propertyPath(keyframePath, "y"), 0, 1, errors);
917
+ optionalString(keyframe, "easing", propertyPath(keyframePath, "easing"), errors);
918
+ if (frame !== undefined) {
919
+ if (frame <= previousFrame) {
920
+ errors.push(validationError("SCHEMA_KEYFRAMES_UNSORTED", propertyPath(keyframePath, "frame"), "crop keyframes must be sorted by increasing frame."));
921
+ }
922
+ if (duration !== undefined && frame >= duration) {
923
+ errors.push(validationError("SCHEMA_FRAME_RANGE_INVALID", propertyPath(keyframePath, "frame"), "crop keyframe frame must be inside the layer duration."));
924
+ }
925
+ previousFrame = frame;
926
+ }
927
+ });
928
+ }
929
+ function validateKeyframes(value, path, layerDuration, errors) {
930
+ if (value === undefined) {
931
+ return;
932
+ }
933
+ if (!isRecord(value)) {
934
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "keyframes must be an object."));
935
+ return;
936
+ }
937
+ const duration = isInteger(layerDuration) && layerDuration >= 1 ? layerDuration : undefined;
938
+ for (const [property, frames] of Object.entries(value)) {
939
+ const propertyPathValue = propertyPath(path, property);
940
+ if (!isAnimatableProperty(property)) {
941
+ errors.push(validationError("SCHEMA_INVALID_KEYFRAME_PROPERTY", propertyPathValue, `"${property}" is not an MVP animatable property.`, "Use opacity, x, y, scale, or rotation."));
942
+ continue;
943
+ }
944
+ if (!Array.isArray(frames)) {
945
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPathValue, "keyframe track must be an array."));
946
+ continue;
947
+ }
948
+ let previousFrame;
949
+ frames.forEach((keyframe, index) => {
950
+ const keyframePath = indexPath(propertyPathValue, index);
951
+ if (!isRecord(keyframe)) {
952
+ errors.push(validationError("SCHEMA_INVALID_FIELD", keyframePath, "keyframe must be an object."));
953
+ return;
954
+ }
955
+ const frame = requireInteger(keyframe, "frame", propertyPath(keyframePath, "frame"), 0, duration, errors);
956
+ validateKeyframeValue(keyframe.value, property, propertyPath(keyframePath, "value"), errors);
957
+ validateEasing(keyframe.easing, propertyPath(keyframePath, "easing"), errors);
958
+ validateTiming(keyframe.timing, propertyPath(keyframePath, "timing"), errors);
959
+ if (frame !== undefined && previousFrame !== undefined && frame <= previousFrame) {
960
+ errors.push(validationError("SCHEMA_KEYFRAMES_UNSORTED", propertyPath(keyframePath, "frame"), "keyframes must be sorted by increasing frame.", "Sort keyframes by local frame and remove duplicate frame values."));
961
+ }
962
+ if (frame !== undefined) {
963
+ previousFrame = frame;
964
+ }
965
+ });
966
+ }
967
+ }
968
+ function validateKeyframeValue(value, property, path, errors) {
969
+ if (typeof value !== "number" || !Number.isFinite(value)) {
970
+ errors.push(validationError("SCHEMA_KEYFRAME_VALUE_TYPE", path, `${property} keyframe value must be a finite number.`));
971
+ return;
972
+ }
973
+ if (property === "opacity" && (value < 0 || value > 1)) {
974
+ errors.push(validationError("SCHEMA_KEYFRAME_VALUE_RANGE", path, "opacity keyframe value must be between 0 and 1."));
975
+ }
976
+ if (property === "scale" && value < 0) {
977
+ errors.push(validationError("SCHEMA_KEYFRAME_VALUE_RANGE", path, "scale keyframe value must be greater than or equal to 0."));
978
+ }
979
+ }
980
+ function validateEasing(value, path, errors) {
981
+ if (value === undefined) {
982
+ return;
983
+ }
984
+ if (typeof value !== "string") {
985
+ errors.push(validationError("SCHEMA_INVALID_EASING", path, "easing must be a string."));
986
+ return;
987
+ }
988
+ if (EASING_VALUES.has(value) || CUBIC_BEZIER_PATTERN.test(value)) {
989
+ return;
990
+ }
991
+ errors.push(validationError("SCHEMA_INVALID_EASING", path, `easing "${value}" is not supported.`, "Use a named deterministic easing or cubic-bezier(x1,y1,x2,y2)."));
992
+ }
993
+ function validateTiming(value, path, errors, depth = 0) {
994
+ if (value === undefined) {
995
+ return;
996
+ }
997
+ if (!isRecord(value)) {
998
+ errors.push(validationError("SCHEMA_INVALID_TIMING", path, "timing must be an object."));
999
+ return;
1000
+ }
1001
+ if (depth > 6) {
1002
+ errors.push(validationError("SCHEMA_INVALID_TIMING", path, "timing objects must not be nested more than six levels deep."));
1003
+ return;
1004
+ }
1005
+ const type = requireString(value, "type", propertyPath(path, "type"), errors);
1006
+ if (type === undefined) {
1007
+ return;
1008
+ }
1009
+ if (!TIMING_TYPES.has(type)) {
1010
+ errors.push(validationError("SCHEMA_INVALID_TIMING", propertyPath(path, "type"), `timing type "${type}" is not supported.`));
1011
+ return;
1012
+ }
1013
+ switch (type) {
1014
+ case "tween":
1015
+ optionalInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
1016
+ validateEasing(value.easing, propertyPath(path, "easing"), errors);
1017
+ break;
1018
+ case "spring":
1019
+ optionalInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
1020
+ optionalNumber(value, "stiffness", propertyPath(path, "stiffness"), 0, undefined, errors);
1021
+ optionalNumber(value, "damping", propertyPath(path, "damping"), 0, undefined, errors);
1022
+ optionalNumber(value, "mass", propertyPath(path, "mass"), 0, undefined, errors);
1023
+ validatePositiveTimingNumber(value.stiffness, propertyPath(path, "stiffness"), errors);
1024
+ validatePositiveTimingNumber(value.damping, propertyPath(path, "damping"), errors);
1025
+ validatePositiveTimingNumber(value.mass, propertyPath(path, "mass"), errors);
1026
+ optionalNumber(value, "restSpeed", propertyPath(path, "restSpeed"), 0, undefined, errors);
1027
+ optionalNumber(value, "bounce", propertyPath(path, "bounce"), 0, 1, errors);
1028
+ break;
1029
+ case "steps":
1030
+ optionalInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
1031
+ requireInteger(value, "steps", propertyPath(path, "steps"), 1, undefined, errors);
1032
+ optionalEnum(value, "direction", propertyPath(path, "direction"), new Set(["start", "end"]), errors);
1033
+ break;
1034
+ case "sequence":
1035
+ validateSequenceTiming(value.segments, propertyPath(path, "segments"), errors, depth);
1036
+ break;
1037
+ case "stagger":
1038
+ validateTiming(value.timing, propertyPath(path, "timing"), errors, depth + 1);
1039
+ requireInteger(value, "childCount", propertyPath(path, "childCount"), 1, undefined, errors);
1040
+ requireInteger(value, "eachFrames", propertyPath(path, "eachFrames"), 0, undefined, errors);
1041
+ optionalInteger(value, "childIndex", propertyPath(path, "childIndex"), 0, undefined, errors);
1042
+ optionalEnum(value, "from", propertyPath(path, "from"), new Set(["start", "center", "end"]), errors);
1043
+ if (isInteger(value.childCount) &&
1044
+ isInteger(value.childIndex) &&
1045
+ value.childIndex >= value.childCount) {
1046
+ errors.push(validationError("SCHEMA_INVALID_TIMING", propertyPath(path, "childIndex"), "stagger childIndex must be lower than childCount."));
1047
+ }
1048
+ break;
1049
+ }
1050
+ }
1051
+ function validatePositiveTimingNumber(value, path, errors) {
1052
+ if (value === 0) {
1053
+ errors.push(validationError("SCHEMA_INVALID_TIMING", path, "timing value must be greater than 0."));
1054
+ }
1055
+ }
1056
+ function validateSequenceTiming(value, path, errors, depth) {
1057
+ if (!Array.isArray(value) || value.length === 0) {
1058
+ errors.push(validationError("SCHEMA_INVALID_TIMING", path, "sequence timing must include at least one segment."));
1059
+ return;
1060
+ }
1061
+ value.forEach((segment, index) => {
1062
+ const segmentPath = indexPath(path, index);
1063
+ if (!isRecord(segment)) {
1064
+ errors.push(validationError("SCHEMA_INVALID_TIMING", segmentPath, "sequence segment must be an object."));
1065
+ return;
1066
+ }
1067
+ requireInteger(segment, "durationFrames", propertyPath(segmentPath, "durationFrames"), 1, undefined, errors);
1068
+ validateTiming(segment.timing, propertyPath(segmentPath, "timing"), errors, depth + 1);
1069
+ optionalNumber(segment, "from", propertyPath(segmentPath, "from"), undefined, undefined, errors);
1070
+ optionalNumber(segment, "to", propertyPath(segmentPath, "to"), undefined, undefined, errors);
1071
+ });
1072
+ }
1073
+ function timingProvidesDuration(value) {
1074
+ if (!isRecord(value) || typeof value.type !== "string") {
1075
+ return false;
1076
+ }
1077
+ switch (value.type) {
1078
+ case "tween":
1079
+ case "spring":
1080
+ case "steps":
1081
+ return isInteger(value.durationFrames) && value.durationFrames >= 1;
1082
+ case "sequence":
1083
+ return (Array.isArray(value.segments) &&
1084
+ value.segments.length > 0 &&
1085
+ value.segments.every((segment) => isRecord(segment) && isInteger(segment.durationFrames) && segment.durationFrames >= 1));
1086
+ case "stagger":
1087
+ return timingProvidesDuration(value.timing);
1088
+ default:
1089
+ return false;
1090
+ }
1091
+ }
1092
+ function validateInterpolations(input, props, errors) {
1093
+ walkStrings(input, "", (value, path) => {
1094
+ const matches = [...value.matchAll(PLACEHOLDER_PATTERN)];
1095
+ const hasInterpolationSyntax = value.includes("{{") || value.includes("}}");
1096
+ if (!hasInterpolationSyntax) {
1097
+ return;
1098
+ }
1099
+ if (matches.length === 0) {
1100
+ errors.push(validationError("PROP_UNRESOLVED", path, "string contains unresolved prop interpolation syntax.", "Use placeholders like {{propName}} and declare the prop in props."));
1101
+ return;
1102
+ }
1103
+ const stripped = value.replace(PLACEHOLDER_PATTERN, "");
1104
+ if (stripped.includes("{{") || stripped.includes("}}")) {
1105
+ errors.push(validationError("PROP_UNRESOLVED", path, "string contains unresolved prop interpolation syntax.", "Check for unmatched braces or nested placeholders."));
1106
+ }
1107
+ for (const match of matches) {
1108
+ const rawName = match[1];
1109
+ const propName = rawName?.trim() ?? "";
1110
+ if (!PROP_NAME_PATTERN.test(propName)) {
1111
+ errors.push(validationError("PROP_UNRESOLVED", path, `prop placeholder "{{${rawName ?? ""}}}" is not a valid prop name.`));
1112
+ continue;
1113
+ }
1114
+ const prop = props.get(propName);
1115
+ if (prop === undefined) {
1116
+ errors.push(validationError("PROP_UNDECLARED_PLACEHOLDER", path, `placeholder references undeclared prop "${propName}".`, `Declare props.${propName} or remove the placeholder.`));
1117
+ continue;
1118
+ }
1119
+ const allowedTypes = allowedPropTypesForPath(path, value, match[0]);
1120
+ if (allowedTypes !== undefined && !allowedTypes.has(prop.type)) {
1121
+ errors.push(validationError("PROP_TYPE_MISMATCH", path, `prop "${propName}" has type ${prop.type}, which is not valid for this field.`, `Use a prop of type ${Array.from(allowedTypes).join(" or ")} here.`));
1122
+ }
1123
+ }
1124
+ });
1125
+ }
1126
+ function allowedPropTypesForPath(path, value, placeholder) {
1127
+ const isFullPlaceholder = value.trim() === placeholder;
1128
+ if (path.endsWith(".src") && path.startsWith("assets.")) {
1129
+ return new Set(["url", "string"]);
1130
+ }
1131
+ if (path.endsWith(".asset")) {
1132
+ return new Set(["asset", "string"]);
1133
+ }
1134
+ if (path.endsWith(".fill") ||
1135
+ path.endsWith(".background") ||
1136
+ path.endsWith(".color") ||
1137
+ path.endsWith(".stroke.color") ||
1138
+ path.endsWith(".shadow.color") ||
1139
+ path.endsWith(".highlight.color")) {
1140
+ return new Set(["color", "string"]);
1141
+ }
1142
+ if (isFullPlaceholder &&
1143
+ (path.endsWith(".x") ||
1144
+ path.endsWith(".y") ||
1145
+ path.endsWith(".width") ||
1146
+ path.endsWith(".height") ||
1147
+ path.endsWith(".fontSize") ||
1148
+ path.endsWith(".fontWeight") ||
1149
+ path.endsWith(".lineHeight") ||
1150
+ path.endsWith(".letterSpacing") ||
1151
+ path.endsWith(".padding") ||
1152
+ path.endsWith(".radius"))) {
1153
+ return new Set(["number"]);
1154
+ }
1155
+ return new Set(["string", "url", "color", "enum", "asset"]);
1156
+ }
1157
+ function validatePropValue(value, type, path, options, errors) {
1158
+ switch (type) {
1159
+ case "string":
1160
+ case "color":
1161
+ case "url":
1162
+ case "asset":
1163
+ if (typeof value !== "string") {
1164
+ errors.push(validationError("PROP_TYPE_MISMATCH", path, `default value for ${type} prop must be a string.`));
1165
+ }
1166
+ break;
1167
+ case "number":
1168
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1169
+ errors.push(validationError("PROP_TYPE_MISMATCH", path, "default value for number prop must be a finite number."));
1170
+ }
1171
+ break;
1172
+ case "boolean":
1173
+ if (typeof value !== "boolean") {
1174
+ errors.push(validationError("PROP_TYPE_MISMATCH", path, "default value for boolean prop must be a boolean."));
1175
+ }
1176
+ break;
1177
+ case "enum":
1178
+ if (options !== undefined && !options.includes(value)) {
1179
+ errors.push(validationError("PROP_TYPE_MISMATCH", path, "default value for enum prop must be one of its options."));
1180
+ }
1181
+ break;
1182
+ }
1183
+ }
1184
+ function validateAssetReference(value, path, expectedType, assets, errors) {
1185
+ if (typeof value !== "string") {
1186
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, "asset reference is required."));
1187
+ return;
1188
+ }
1189
+ const asset = assets.get(value);
1190
+ if (asset === undefined) {
1191
+ errors.push(validationError("SCHEMA_UNKNOWN_ASSET_REFERENCE", path, `unknown asset reference "${value}".`));
1192
+ return;
1193
+ }
1194
+ if (expectedType !== undefined && asset.type !== expectedType) {
1195
+ errors.push(validationError("SCHEMA_ASSET_TYPE_MISMATCH", path, `asset "${value}" has type ${asset.type}; expected ${expectedType}.`, `Use a ${expectedType} asset id here or change ${asset.path}.type.`));
1196
+ }
1197
+ }
1198
+ function validateFrameRange(path, startFrame, durationFrames, compositionDuration, errors) {
1199
+ if (startFrame === undefined || durationFrames === undefined) {
1200
+ return;
1201
+ }
1202
+ if (!Number.isSafeInteger(startFrame + durationFrames)) {
1203
+ errors.push(validationError("SCHEMA_FRAME_RANGE_INVALID", path, "frame range exceeds safe integer limits."));
1204
+ return;
1205
+ }
1206
+ if (compositionDuration !== undefined && startFrame + durationFrames > compositionDuration) {
1207
+ errors.push(validationError("SCHEMA_FRAME_RANGE_INVALID", path, "frame range extends beyond composition.durationFrames.", "Set startFrame + durationFrames to be less than or equal to composition.durationFrames."));
1208
+ }
1209
+ }
1210
+ function validatePosition(value, path, errors) {
1211
+ if (value === undefined) {
1212
+ return;
1213
+ }
1214
+ if (!isRecord(value)) {
1215
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "position must be an object."));
1216
+ return;
1217
+ }
1218
+ optionalCoordinate(value, "x", propertyPath(path, "x"), errors);
1219
+ optionalCoordinate(value, "y", propertyPath(path, "y"), errors);
1220
+ }
1221
+ function validateSize(value, path, errors) {
1222
+ if (value === undefined) {
1223
+ return;
1224
+ }
1225
+ if (!isRecord(value)) {
1226
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "size must be an object."));
1227
+ return;
1228
+ }
1229
+ optionalCoordinate(value, "width", propertyPath(path, "width"), errors);
1230
+ optionalCoordinate(value, "height", propertyPath(path, "height"), errors);
1231
+ }
1232
+ function validateAnchor(value, path, errors) {
1233
+ if (value === undefined) {
1234
+ return;
1235
+ }
1236
+ if (typeof value === "string") {
1237
+ if (!ANCHOR_VALUES.has(value)) {
1238
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `anchor "${value}" is not supported.`, "Use a named anchor or {x,y} fractions."));
1239
+ }
1240
+ return;
1241
+ }
1242
+ if (isRecord(value)) {
1243
+ requireNumber(value, "x", propertyPath(path, "x"), 0, 1, errors);
1244
+ requireNumber(value, "y", propertyPath(path, "y"), 0, 1, errors);
1245
+ return;
1246
+ }
1247
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "anchor must be a string or an {x,y} object."));
1248
+ }
1249
+ function validateTransition(value, path, errors) {
1250
+ if (value === undefined || value === null) {
1251
+ return;
1252
+ }
1253
+ if (!isRecord(value)) {
1254
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, "transition must be an object or null."));
1255
+ return;
1256
+ }
1257
+ const type = requireString(value, "type", propertyPath(path, "type"), errors);
1258
+ if (type !== undefined && !TRANSITION_TYPES.has(type)) {
1259
+ errors.push(validationError("SCHEMA_INVALID_FIELD", propertyPath(path, "type"), "type has an unsupported value."));
1260
+ }
1261
+ if (value.timing === undefined) {
1262
+ requireInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
1263
+ }
1264
+ else {
1265
+ optionalInteger(value, "durationFrames", propertyPath(path, "durationFrames"), 1, undefined, errors);
1266
+ validateTiming(value.timing, propertyPath(path, "timing"), errors);
1267
+ if (value.durationFrames === undefined && !timingProvidesDuration(value.timing)) {
1268
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", propertyPath(path, "durationFrames"), "transition durationFrames is required when timing does not declare a duration."));
1269
+ }
1270
+ }
1271
+ optionalEnum(value, "direction", propertyPath(path, "direction"), TRANSITION_DIRECTIONS, errors);
1272
+ optionalEnum(value, "axis", propertyPath(path, "axis"), TRANSITION_AXES, errors);
1273
+ optionalEnum(value, "shape", propertyPath(path, "shape"), TRANSITION_SHAPES, errors);
1274
+ optionalString(value, "color", propertyPath(path, "color"), errors);
1275
+ optionalNumber(value, "amount", propertyPath(path, "amount"), 0, undefined, errors);
1276
+ optionalNumber(value, "intensity", propertyPath(path, "intensity"), 0, undefined, errors);
1277
+ optionalInteger(value, "rows", propertyPath(path, "rows"), 1, 32, errors);
1278
+ optionalInteger(value, "columns", propertyPath(path, "columns"), 1, 32, errors);
1279
+ validateEasing(value.easing, propertyPath(path, "easing"), errors);
1280
+ }
1281
+ function validateRegisteredFont(fontFamily, path, assets, errors) {
1282
+ const normalizedFamily = fontFamily.toLowerCase();
1283
+ if (WEB_SAFE_FONT_FAMILIES.has(normalizedFamily)) {
1284
+ return;
1285
+ }
1286
+ for (const asset of assets.values()) {
1287
+ if (asset.type === "font" && asset.family?.toLowerCase() === normalizedFamily) {
1288
+ return;
1289
+ }
1290
+ }
1291
+ errors.push(validationError("SCHEMA_FONT_NOT_REGISTERED", path, `fontFamily "${fontFamily}" does not match a registered font asset or known web-safe family.`, "Add a font asset with the matching family or use a web-safe family."));
1292
+ }
1293
+ function requireString(record, key, path, errors) {
1294
+ if (!(key in record)) {
1295
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, `${key} is required.`));
1296
+ return undefined;
1297
+ }
1298
+ const value = record[key];
1299
+ if (typeof value !== "string") {
1300
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} must be a string.`));
1301
+ return undefined;
1302
+ }
1303
+ if (value.length === 0) {
1304
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} must not be empty.`));
1305
+ }
1306
+ return value;
1307
+ }
1308
+ function requireInteger(record, key, path, min, max, errors) {
1309
+ if (!(key in record)) {
1310
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, `${key} is required.`));
1311
+ return undefined;
1312
+ }
1313
+ return validateIntegerValue(record[key], path, `${key} must be an integer.`, min, max, errors);
1314
+ }
1315
+ function requireNumber(record, key, path, min, max, errors) {
1316
+ if (!(key in record)) {
1317
+ errors.push(validationError("SCHEMA_REQUIRED_FIELD", path, `${key} is required.`));
1318
+ return undefined;
1319
+ }
1320
+ return validateNumberValue(record[key], path, `${key} must be a finite number.`, min, max, errors);
1321
+ }
1322
+ function optionalString(record, key, path, errors, nullable = false) {
1323
+ const value = record[key];
1324
+ if (value === undefined || (nullable && value === null)) {
1325
+ return undefined;
1326
+ }
1327
+ if (typeof value !== "string") {
1328
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} must be a string${nullable ? " or null" : ""}.`));
1329
+ return undefined;
1330
+ }
1331
+ return value;
1332
+ }
1333
+ function optionalBoolean(record, key, path, errors) {
1334
+ const value = record[key];
1335
+ if (value === undefined) {
1336
+ return undefined;
1337
+ }
1338
+ if (typeof value !== "boolean") {
1339
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} must be a boolean.`));
1340
+ return undefined;
1341
+ }
1342
+ return value;
1343
+ }
1344
+ function optionalInteger(record, key, path, min, max, errors, nullable = false) {
1345
+ const value = record[key];
1346
+ if (value === undefined || (nullable && value === null)) {
1347
+ return undefined;
1348
+ }
1349
+ return validateIntegerValue(value, path, `${key} must be an integer${nullable ? " or null" : ""}.`, min, max, errors);
1350
+ }
1351
+ function optionalNumber(record, key, path, min, max, errors) {
1352
+ const value = record[key];
1353
+ if (value === undefined) {
1354
+ return undefined;
1355
+ }
1356
+ return validateNumberValue(value, path, `${key} must be a finite number.`, min, max, errors);
1357
+ }
1358
+ function optionalNumberOrString(record, key, path, min, max, errors) {
1359
+ const value = record[key];
1360
+ if (value === undefined) {
1361
+ return undefined;
1362
+ }
1363
+ if (typeof value === "string" && value.length > 0) {
1364
+ return value;
1365
+ }
1366
+ return validateNumberValue(value, path, `${key} must be a finite number or non-empty string.`, min, max, errors);
1367
+ }
1368
+ function optionalCoordinate(record, key, path, errors) {
1369
+ const value = record[key];
1370
+ if (value === undefined) {
1371
+ return;
1372
+ }
1373
+ if (typeof value === "number" && Number.isFinite(value)) {
1374
+ return;
1375
+ }
1376
+ if (typeof value === "string" && (/^-?(?:\d+|\d*\.\d+)%(?:w|h)?$/.test(value) || /{{\s*([^{}]+?)\s*}}/.test(value))) {
1377
+ return;
1378
+ }
1379
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} must be a number or percentage string.`));
1380
+ }
1381
+ function optionalEnum(record, key, path, allowed, errors) {
1382
+ const value = record[key];
1383
+ if (value === undefined) {
1384
+ return undefined;
1385
+ }
1386
+ if (typeof value !== "string" || !allowed.has(value)) {
1387
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, `${key} has an unsupported value.`));
1388
+ return undefined;
1389
+ }
1390
+ return value;
1391
+ }
1392
+ function validateIntegerValue(value, path, message, min, max, errors) {
1393
+ if (!isInteger(value)) {
1394
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, message));
1395
+ return undefined;
1396
+ }
1397
+ if ((min !== undefined && value < min) || (max !== undefined && value > max)) {
1398
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, rangeMessage(path, min, max)));
1399
+ return undefined;
1400
+ }
1401
+ return value;
1402
+ }
1403
+ function validateNumberValue(value, path, message, min, max, errors) {
1404
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1405
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, message));
1406
+ return undefined;
1407
+ }
1408
+ if ((min !== undefined && value < min) || (max !== undefined && value > max)) {
1409
+ errors.push(validationError("SCHEMA_INVALID_FIELD", path, rangeMessage(path, min, max)));
1410
+ return undefined;
1411
+ }
1412
+ return value;
1413
+ }
1414
+ function rangeMessage(path, min, max) {
1415
+ const fieldName = path.slice(path.lastIndexOf(".") + 1);
1416
+ if (min !== undefined && max !== undefined) {
1417
+ return `${fieldName} must be between ${min} and ${max}.`;
1418
+ }
1419
+ if (min !== undefined) {
1420
+ return `${fieldName} must be greater than or equal to ${min}.`;
1421
+ }
1422
+ return `${fieldName} must be less than or equal to ${max}.`;
1423
+ }
1424
+ function walkStrings(value, path, visit) {
1425
+ if (typeof value === "string") {
1426
+ visit(value, path);
1427
+ return;
1428
+ }
1429
+ if (Array.isArray(value)) {
1430
+ value.forEach((item, index) => {
1431
+ walkStrings(item, indexPath(path, index), visit);
1432
+ });
1433
+ return;
1434
+ }
1435
+ if (isRecord(value)) {
1436
+ for (const [key, child] of Object.entries(value)) {
1437
+ walkStrings(child, propertyPath(path, key), visit);
1438
+ }
1439
+ }
1440
+ }
1441
+ function validationError(code, path, message, hint) {
1442
+ return kavioError(code, "error", path, message, hint);
1443
+ }
1444
+ function validationWarning(code, path, message, hint) {
1445
+ return kavioError(code, "warning", path, message, hint);
1446
+ }
1447
+ function kavioError(code, severity, path, message, hint) {
1448
+ const error = {
1449
+ code,
1450
+ severity,
1451
+ message,
1452
+ path,
1453
+ stage: "validation",
1454
+ retryable: false
1455
+ };
1456
+ if (hint !== undefined) {
1457
+ error.hint = hint;
1458
+ }
1459
+ return error;
1460
+ }
1461
+ function propertyPath(parent, key) {
1462
+ const segment = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `[${JSON.stringify(key)}]`;
1463
+ if (!parent) {
1464
+ return segment;
1465
+ }
1466
+ return segment.startsWith("[") ? `${parent}${segment}` : `${parent}.${segment}`;
1467
+ }
1468
+ function indexPath(parent, index) {
1469
+ return `${parent}[${index}]`;
1470
+ }
1471
+ function isRecord(value) {
1472
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1473
+ }
1474
+ function isInteger(value) {
1475
+ return Number.isSafeInteger(value);
1476
+ }
1477
+ function isAssetType(value) {
1478
+ return ASSET_TYPES.has(value);
1479
+ }
1480
+ function isLayerType(value) {
1481
+ return LAYER_TYPES.has(value);
1482
+ }
1483
+ function isPropType(value) {
1484
+ return PROP_TYPES.has(value);
1485
+ }
1486
+ function isExportFormat(value) {
1487
+ return typeof value === "string" && EXPORT_FORMATS.has(value);
1488
+ }
1489
+ function isAnimatableProperty(value) {
1490
+ return ANIMATABLE_PROPERTIES.has(value);
1491
+ }