@velumo/core 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/background-validation.d.ts +3 -0
  2. package/dist/background-validation.d.ts.map +1 -0
  3. package/dist/background-validation.js +177 -0
  4. package/dist/background-validation.js.map +1 -0
  5. package/dist/builders.d.ts +134 -0
  6. package/dist/builders.d.ts.map +1 -0
  7. package/dist/builders.js +332 -0
  8. package/dist/builders.js.map +1 -0
  9. package/dist/capabilities.d.ts +7 -0
  10. package/dist/capabilities.d.ts.map +1 -0
  11. package/dist/capabilities.js +59 -0
  12. package/dist/capabilities.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/layout-validation.d.ts +4 -0
  18. package/dist/layout-validation.d.ts.map +1 -0
  19. package/dist/layout-validation.js +745 -0
  20. package/dist/layout-validation.js.map +1 -0
  21. package/dist/manifest-schema.d.ts +1328 -0
  22. package/dist/manifest-schema.d.ts.map +1 -0
  23. package/dist/manifest-schema.js +774 -0
  24. package/dist/manifest-schema.js.map +1 -0
  25. package/dist/theme-validation.d.ts +3 -0
  26. package/dist/theme-validation.d.ts.map +1 -0
  27. package/dist/theme-validation.js +239 -0
  28. package/dist/theme-validation.js.map +1 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -0
  30. package/dist/types.d.ts +398 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/validation-issue.d.ts +18 -0
  35. package/dist/validation-issue.d.ts.map +1 -0
  36. package/dist/validation-issue.js +30 -0
  37. package/dist/validation-issue.js.map +1 -0
  38. package/dist/validation.d.ts +4 -0
  39. package/dist/validation.d.ts.map +1 -0
  40. package/dist/validation.js +274 -0
  41. package/dist/validation.js.map +1 -0
  42. package/package.json +16 -0
  43. package/src/background-validation.ts +315 -0
  44. package/src/builders.ts +626 -0
  45. package/src/capabilities.ts +99 -0
  46. package/src/index.ts +147 -0
  47. package/src/layout-validation.ts +1385 -0
  48. package/src/manifest-schema.ts +823 -0
  49. package/src/theme-validation.ts +410 -0
  50. package/src/types.ts +560 -0
  51. package/src/validation-issue.ts +43 -0
  52. package/src/validation.ts +468 -0
  53. package/tsconfig.json +9 -0
@@ -0,0 +1,274 @@
1
+ import { validateBackground } from "./background-validation.js";
2
+ import { validateLayoutNode } from "./layout-validation.js";
3
+ import { validateTheme } from "./theme-validation.js";
4
+ import { describeType, issue } from "./validation-issue.js";
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function isNonEmptyString(value) {
9
+ return typeof value === "string" && value.trim().length > 0;
10
+ }
11
+ function typeIssue(path, message, expected, received) {
12
+ return issue(path, "wrong_type", message, {
13
+ expected,
14
+ received: describeType(received),
15
+ });
16
+ }
17
+ function requiredIssue(path, message) {
18
+ return issue(path, "required", message, {
19
+ suggestion: `Provide a value for \`${path}\`.`,
20
+ });
21
+ }
22
+ const CAMERA_EASINGS = [
23
+ "linear",
24
+ "ease",
25
+ "ease-in",
26
+ "ease-out",
27
+ "ease-in-out",
28
+ ];
29
+ const CAMERA_VIEW_KEYS = new Set(["x", "y", "zoom"]);
30
+ const CAMERA_TRANSITION_KEYS = new Set(["duration", "easing"]);
31
+ const CAMERA_STOP_KEYS = new Set(["camera", "transition", "cues", "hold"]);
32
+ function isPercent(value) {
33
+ return (typeof value === "number" &&
34
+ Number.isFinite(value) &&
35
+ value >= 0 &&
36
+ value <= 100);
37
+ }
38
+ function valueIssue(path, message, received) {
39
+ return issue(path, "invalid_value", message, {
40
+ received: describeType(received),
41
+ });
42
+ }
43
+ function unknownFieldIssues(record, allowed, path) {
44
+ return Object.keys(record)
45
+ .filter((key) => !allowed.has(key))
46
+ .map((key) => issue(`${path}.${key}`, "unknown_field", `${path}.${key} is not allowed`, {
47
+ expected: [...allowed].join(", "),
48
+ suggestion: `Remove \`${key}\`; it is not a recognized field here.`,
49
+ }));
50
+ }
51
+ function validateCameraView(view, path) {
52
+ if (!isRecord(view)) {
53
+ return [typeIssue(path, `${path} must be an object`, "object", view)];
54
+ }
55
+ const issues = [];
56
+ for (const axis of ["x", "y"]) {
57
+ if (view[axis] === undefined) {
58
+ issues.push(requiredIssue(`${path}.${axis}`, `${path}.${axis} is required`));
59
+ }
60
+ else if (!isPercent(view[axis])) {
61
+ issues.push(valueIssue(`${path}.${axis}`, `${path}.${axis} must be a percentage 0-100`, view[axis]));
62
+ }
63
+ }
64
+ if (view.zoom !== undefined) {
65
+ if (typeof view.zoom !== "number" ||
66
+ !Number.isFinite(view.zoom) ||
67
+ view.zoom <= 0) {
68
+ issues.push(valueIssue(`${path}.zoom`, `${path}.zoom must be a number > 0`, view.zoom));
69
+ }
70
+ }
71
+ issues.push(...unknownFieldIssues(view, CAMERA_VIEW_KEYS, path));
72
+ return issues;
73
+ }
74
+ function validateCameraTransition(transition, path) {
75
+ if (!isRecord(transition)) {
76
+ return [typeIssue(path, `${path} must be an object`, "object", transition)];
77
+ }
78
+ const issues = [];
79
+ if (transition.duration !== undefined) {
80
+ if (typeof transition.duration !== "number" ||
81
+ !Number.isFinite(transition.duration) ||
82
+ transition.duration < 0) {
83
+ issues.push(valueIssue(`${path}.duration`, `${path}.duration must be a number >= 0`, transition.duration));
84
+ }
85
+ }
86
+ if (transition.easing !== undefined &&
87
+ !CAMERA_EASINGS.includes(transition.easing)) {
88
+ issues.push(issue(`${path}.easing`, "invalid_enum", `${path}.easing must be a known easing`, {
89
+ expected: CAMERA_EASINGS.join(", "),
90
+ received: typeof transition.easing === "string"
91
+ ? transition.easing
92
+ : describeType(transition.easing),
93
+ suggestion: `Use one of: ${CAMERA_EASINGS.join(", ")}.`,
94
+ }));
95
+ }
96
+ issues.push(...unknownFieldIssues(transition, CAMERA_TRANSITION_KEYS, path));
97
+ return issues;
98
+ }
99
+ function validateCameraStop(stop, path) {
100
+ if (!isRecord(stop)) {
101
+ return [typeIssue(path, `${path} must be an object`, "object", stop)];
102
+ }
103
+ const issues = [];
104
+ if (stop.camera === undefined) {
105
+ issues.push(requiredIssue(`${path}.camera`, `${path}.camera is required`));
106
+ }
107
+ else {
108
+ issues.push(...validateCameraView(stop.camera, `${path}.camera`));
109
+ }
110
+ if (stop.transition !== undefined) {
111
+ issues.push(...validateCameraTransition(stop.transition, `${path}.transition`));
112
+ }
113
+ if (stop.cues !== undefined) {
114
+ if (!Array.isArray(stop.cues)) {
115
+ issues.push(typeIssue(`${path}.cues`, `${path}.cues must be an array`, "array", stop.cues));
116
+ }
117
+ else {
118
+ stop.cues.forEach((cue, cueIndex) => {
119
+ const cuePath = `${path}.cues[${cueIndex}]`;
120
+ if (!isRecord(cue)) {
121
+ issues.push(typeIssue(cuePath, `${cuePath} must be an object`, "object", cue));
122
+ return;
123
+ }
124
+ if (typeof cue.time !== "number") {
125
+ issues.push(requiredIssue(`${cuePath}.time`, `${cuePath}.time is required`));
126
+ }
127
+ else if (!Number.isFinite(cue.time) || cue.time < 0) {
128
+ issues.push(valueIssue(`${cuePath}.time`, `${cuePath}.time must be a finite number >= 0`, cue.time));
129
+ }
130
+ if (!isRecord(cue.action) || typeof cue.action.type !== "string") {
131
+ issues.push(requiredIssue(`${cuePath}.action`, `${cuePath}.action.type is required`));
132
+ }
133
+ });
134
+ }
135
+ }
136
+ if (stop.hold !== undefined) {
137
+ if (typeof stop.hold !== "number" ||
138
+ !Number.isFinite(stop.hold) ||
139
+ stop.hold < 0) {
140
+ issues.push(valueIssue(`${path}.hold`, `${path}.hold must be a number >= 0`, stop.hold));
141
+ }
142
+ }
143
+ issues.push(...unknownFieldIssues(stop, CAMERA_STOP_KEYS, path));
144
+ return issues;
145
+ }
146
+ function validateCameraPath(value, path) {
147
+ if (!Array.isArray(value)) {
148
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
149
+ }
150
+ const issues = [];
151
+ if (value.length === 0) {
152
+ issues.push(issue(path, "constraint", `${path} must contain at least one stop`, {
153
+ suggestion: "Add a cameraStop, or remove the empty cameraPath.",
154
+ }));
155
+ return issues;
156
+ }
157
+ value.forEach((stop, index) => {
158
+ issues.push(...validateCameraStop(stop, `${path}[${index}]`));
159
+ });
160
+ return issues;
161
+ }
162
+ function validateSlideEntry(entry, index) {
163
+ const issues = [];
164
+ const path = `slides[${index}]`;
165
+ if (!isRecord(entry)) {
166
+ return [typeIssue(path, `${path} must be an object`, "object", entry)];
167
+ }
168
+ if (!isNonEmptyString(entry.id)) {
169
+ issues.push(requiredIssue(`${path}.id`, `${path}.id is required`));
170
+ }
171
+ if (entry.layout !== undefined) {
172
+ issues.push(...validateLayoutNode(entry.layout, `${path}.layout`));
173
+ }
174
+ if (Array.isArray(entry.layers)) {
175
+ entry.layers.forEach((layer, layerIndex) => {
176
+ const layerPath = `${path}.layers[${layerIndex}]`;
177
+ if (!isRecord(layer)) {
178
+ issues.push(typeIssue(layerPath, `${layerPath} must be an object`, "object", layer));
179
+ return;
180
+ }
181
+ if (!isNonEmptyString(layer.type)) {
182
+ issues.push(requiredIssue(`${layerPath}.type`, `${layerPath}.type is required`));
183
+ }
184
+ });
185
+ }
186
+ if (Array.isArray(entry.cues)) {
187
+ entry.cues.forEach((cue, cueIndex) => {
188
+ const cuePath = `${path}.cues[${cueIndex}]`;
189
+ if (!isRecord(cue)) {
190
+ issues.push(typeIssue(cuePath, `${cuePath} must be an object`, "object", cue));
191
+ return;
192
+ }
193
+ if (typeof cue.time !== "number") {
194
+ issues.push(requiredIssue(`${cuePath}.time`, `${cuePath}.time is required`));
195
+ }
196
+ else if (!Number.isFinite(cue.time) || cue.time < 0) {
197
+ issues.push(valueIssue(`${cuePath}.time`, `${cuePath}.time must be a finite number >= 0`, cue.time));
198
+ }
199
+ });
200
+ }
201
+ if (Array.isArray(entry.fragments)) {
202
+ entry.fragments.forEach((fragment, fragmentIndex) => {
203
+ const fragmentPath = `${path}.fragments[${fragmentIndex}]`;
204
+ if (!isRecord(fragment)) {
205
+ issues.push(typeIssue(fragmentPath, `${fragmentPath} must be an object`, "object", fragment));
206
+ return;
207
+ }
208
+ if (!isNonEmptyString(fragment.id)) {
209
+ issues.push(requiredIssue(`${fragmentPath}.id`, `${fragmentPath}.id is required`));
210
+ }
211
+ });
212
+ }
213
+ if (entry.background !== undefined) {
214
+ issues.push(...validateBackground(entry.background, `${path}.background`));
215
+ }
216
+ if (entry.cameraPath !== undefined) {
217
+ if (entry.kind !== "scene") {
218
+ issues.push(issue(`${path}.cameraPath`, "unknown_field", `${path}.cameraPath is only allowed on a scene (kind: "scene")`, {
219
+ suggestion: `Change this entry's kind to "scene", or remove cameraPath.`,
220
+ }));
221
+ }
222
+ else {
223
+ issues.push(...validateCameraPath(entry.cameraPath, `${path}.cameraPath`));
224
+ }
225
+ }
226
+ if (entry.chrome !== undefined && typeof entry.chrome !== "boolean") {
227
+ issues.push(typeIssue(`${path}.chrome`, `${path}.chrome must be a boolean`, "boolean", entry.chrome));
228
+ }
229
+ return issues;
230
+ }
231
+ function collectIssues(manifest) {
232
+ if (!isRecord(manifest)) {
233
+ return [
234
+ typeIssue("manifest", "manifest must be an object", "object", manifest),
235
+ ];
236
+ }
237
+ const issues = [];
238
+ if (!isRecord(manifest.deck)) {
239
+ issues.push(typeIssue("deck", "deck must be an object", "object", manifest.deck));
240
+ }
241
+ else {
242
+ if (!isNonEmptyString(manifest.deck.title)) {
243
+ issues.push(requiredIssue("deck.title", "deck.title is required"));
244
+ }
245
+ if (manifest.deck.watermark !== undefined &&
246
+ typeof manifest.deck.watermark !== "string") {
247
+ issues.push(typeIssue("deck.watermark", "deck.watermark must be a string", "string", manifest.deck.watermark));
248
+ }
249
+ }
250
+ if (!Array.isArray(manifest.slides)) {
251
+ issues.push(typeIssue("slides", "slides must be an array", "array", manifest.slides));
252
+ }
253
+ else {
254
+ manifest.slides.forEach((slide, index) => {
255
+ issues.push(...validateSlideEntry(slide, index));
256
+ });
257
+ }
258
+ if (manifest.theme !== undefined) {
259
+ issues.push(...validateTheme(manifest.theme));
260
+ }
261
+ return issues;
262
+ }
263
+ export function validateManifest(manifest) {
264
+ const issues = collectIssues(manifest);
265
+ return {
266
+ valid: issues.length === 0,
267
+ errors: issues.map((current) => current.message),
268
+ issues,
269
+ };
270
+ }
271
+ export function isValidManifest(manifest) {
272
+ return validateManifest(manifest).valid;
273
+ }
274
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAI5D,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAChB,IAAY,EACZ,OAAe,EACf,QAAgB,EAChB,QAAiB;IAEjB,OAAO,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE;QACxC,QAAQ;QACR,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC;KACjC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,OAAe;IAClD,OAAO,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE;QACtC,UAAU,EAAE,yBAAyB,IAAI,KAAK;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,MAAM,cAAc,GAAG;IACrB,QAAQ;IACR,MAAM;IACN,SAAS;IACT,UAAU;IACV,aAAa;CACL,CAAC;AAEX,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;AACrD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC/D,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE3E,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtB,KAAK,IAAI,CAAC;QACV,KAAK,IAAI,GAAG,CACb,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CACjB,IAAY,EACZ,OAAe,EACf,QAAiB;IAEjB,OAAO,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE;QAC3C,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC;KACjC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,kBAAkB,CACzB,MAAqB,EACrB,OAA4B,EAC5B,IAAY;IAEZ,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACvB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;SAClC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACX,KAAK,CACH,GAAG,IAAI,IAAI,GAAG,EAAE,EAChB,eAAe,EACf,GAAG,IAAI,IAAI,GAAG,iBAAiB,EAC/B;QACE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,UAAU,EAAE,YAAY,GAAG,wCAAwC;KACpE,CACF,CACF,CAAC;AACN,CAAC;AAED,SAAS,kBAAkB,CACzB,IAAa,EACb,IAAY;IAEZ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,oBAAoB,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAU,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CACT,aAAa,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,GAAG,IAAI,IAAI,IAAI,cAAc,CAAC,CAChE,CAAC;QACJ,CAAC;aAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,IAAI,IAAI,IAAI,EAAE,EACjB,GAAG,IAAI,IAAI,IAAI,6BAA6B,EAC5C,IAAI,CAAC,IAAI,CAAC,CACX,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IACE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC7B,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3B,IAAI,CAAC,IAAI,IAAI,CAAC,EACd,CAAC;YACD,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,IAAI,OAAO,EACd,GAAG,IAAI,4BAA4B,EACnC,IAAI,CAAC,IAAI,CACV,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,wBAAwB,CAC/B,UAAmB,EACnB,IAAY;IAEZ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,oBAAoB,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,IAAI,UAAU,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACtC,IACE,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ;YACvC,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YACrC,UAAU,CAAC,QAAQ,GAAG,CAAC,EACvB,CAAC;YACD,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,IAAI,WAAW,EAClB,GAAG,IAAI,iCAAiC,EACxC,UAAU,CAAC,QAAQ,CACpB,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IACE,UAAU,CAAC,MAAM,KAAK,SAAS;QAC/B,CAAC,cAAc,CAAC,QAAQ,CACtB,UAAU,CAAC,MAAyC,CACrD,EACD,CAAC;QACD,MAAM,CAAC,IAAI,CACT,KAAK,CACH,GAAG,IAAI,SAAS,EAChB,cAAc,EACd,GAAG,IAAI,gCAAgC,EACvC;YACE,QAAQ,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;YACnC,QAAQ,EACN,OAAO,UAAU,CAAC,MAAM,KAAK,QAAQ;gBACnC,CAAC,CAAC,UAAU,CAAC,MAAM;gBACnB,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC;YACrC,UAAU,EAAE,eAAe,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACxD,CACF,CACF,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,EAAE,IAAI,CAAC,CAAC,CAAC;IAC7E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,kBAAkB,CACzB,IAAa,EACb,IAAY;IAEZ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,oBAAoB,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,IAAI,SAAS,EAAE,GAAG,IAAI,qBAAqB,CAAC,CAAC,CAAC;IAC7E,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CACT,GAAG,wBAAwB,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,aAAa,CAAC,CACnE,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CACT,SAAS,CACP,GAAG,IAAI,OAAO,EACd,GAAG,IAAI,wBAAwB,EAC/B,OAAO,EACP,IAAI,CAAC,IAAI,CACV,CACF,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE;gBAClC,MAAM,OAAO,GAAG,GAAG,IAAI,SAAS,QAAQ,GAAG,CAAC;gBAC5C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;oBACnB,MAAM,CAAC,IAAI,CACT,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,oBAAoB,EAAE,QAAQ,EAAE,GAAG,CAAC,CAClE,CAAC;oBACF,OAAO;gBACT,CAAC;gBACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACjC,MAAM,CAAC,IAAI,CACT,aAAa,CAAC,GAAG,OAAO,OAAO,EAAE,GAAG,OAAO,mBAAmB,CAAC,CAChE,CAAC;gBACJ,CAAC;qBAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;oBACtD,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,OAAO,OAAO,EACjB,GAAG,OAAO,oCAAoC,EAC9C,GAAG,CAAC,IAAI,CACT,CACF,CAAC;gBACJ,CAAC;gBACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACjE,MAAM,CAAC,IAAI,CACT,aAAa,CACX,GAAG,OAAO,SAAS,EACnB,GAAG,OAAO,0BAA0B,CACrC,CACF,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IACE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC7B,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3B,IAAI,CAAC,IAAI,GAAG,CAAC,EACb,CAAC;YACD,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,IAAI,OAAO,EACd,GAAG,IAAI,6BAA6B,EACpC,IAAI,CAAC,IAAI,CACV,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,kBAAkB,CACzB,KAAc,EACd,IAAY;IAEZ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,mBAAmB,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,CAAC,IAAI,CACT,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,iCAAiC,EAAE;YAClE,UAAU,EAAE,mDAAmD;SAChE,CAAC,CACH,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,kBAAkB,CACzB,KAAc,EACd,KAAa;IAEb,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,UAAU,KAAK,GAAG,CAAC;IAEhC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,oBAAoB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,IAAI,KAAK,EAAE,GAAG,IAAI,iBAAiB,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;YACzC,MAAM,SAAS,GAAG,GAAG,IAAI,WAAW,UAAU,GAAG,CAAC;YAClD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,CACT,SAAS,CACP,SAAS,EACT,GAAG,SAAS,oBAAoB,EAChC,QAAQ,EACR,KAAK,CACN,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClC,MAAM,CAAC,IAAI,CACT,aAAa,CAAC,GAAG,SAAS,OAAO,EAAE,GAAG,SAAS,mBAAmB,CAAC,CACpE,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE;YACnC,MAAM,OAAO,GAAG,GAAG,IAAI,SAAS,QAAQ,GAAG,CAAC;YAC5C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,MAAM,CAAC,IAAI,CACT,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,oBAAoB,EAAE,QAAQ,EAAE,GAAG,CAAC,CAClE,CAAC;gBACF,OAAO;YACT,CAAC;YAED,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACjC,MAAM,CAAC,IAAI,CACT,aAAa,CAAC,GAAG,OAAO,OAAO,EAAE,GAAG,OAAO,mBAAmB,CAAC,CAChE,CAAC;YACJ,CAAC;iBAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,IAAI,CACT,UAAU,CACR,GAAG,OAAO,OAAO,EACjB,GAAG,OAAO,oCAAoC,EAC9C,GAAG,CAAC,IAAI,CACT,CACF,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE;YAClD,MAAM,YAAY,GAAG,GAAG,IAAI,cAAc,aAAa,GAAG,CAAC;YAC3D,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CACT,SAAS,CACP,YAAY,EACZ,GAAG,YAAY,oBAAoB,EACnC,QAAQ,EACR,QAAQ,CACT,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;gBACnC,MAAM,CAAC,IAAI,CACT,aAAa,CAAC,GAAG,YAAY,KAAK,EAAE,GAAG,YAAY,iBAAiB,CAAC,CACtE,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,IAAI,aAAa,CAAC,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CACT,KAAK,CACH,GAAG,IAAI,aAAa,EACpB,eAAe,EACf,GAAG,IAAI,wDAAwD,EAC/D;gBACE,UAAU,EAAE,4DAA4D;aACzE,CACF,CACF,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CACT,GAAG,kBAAkB,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,IAAI,aAAa,CAAC,CAC9D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACpE,MAAM,CAAC,IAAI,CACT,SAAS,CACP,GAAG,IAAI,SAAS,EAChB,GAAG,IAAI,2BAA2B,EAClC,SAAS,EACT,KAAK,CAAC,MAAM,CACb,CACF,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,QAAiB;IACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,SAAS,CAAC,UAAU,EAAE,4BAA4B,EAAE,QAAQ,EAAE,QAAQ,CAAC;SACxE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,CACT,SAAS,CAAC,MAAM,EAAE,wBAAwB,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CACrE,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC,CAAC;QACrE,CAAC;QACD,IACE,QAAQ,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS;YACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,SAAS,KAAK,QAAQ,EAC3C,CAAC;YACD,MAAM,CAAC,IAAI,CACT,SAAS,CACP,gBAAgB,EAChB,iCAAiC,EACjC,QAAQ,EACR,QAAQ,CAAC,IAAI,CAAC,SAAS,CACxB,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACpC,MAAM,CAAC,IAAI,CACT,SAAS,CAAC,QAAQ,EAAE,yBAAyB,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CACzE,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACvC,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAiB;IAChD,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEvC,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;QAChD,MAAM;KACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAiB;IAC/C,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC;AAC1C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@velumo/core",
3
+ "version": "0.1.0-beta.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -b"
15
+ }
16
+ }
@@ -0,0 +1,315 @@
1
+ import type { ValidationIssue } from "./types.js";
2
+ import { describeType, issue } from "./validation-issue.js";
3
+
4
+ type IndexedRecord = Record<string, unknown>;
5
+
6
+ const BLEND_MODES = ["normal", "screen", "multiply", "overlay"] as const;
7
+ const SHAPE_VALUES = ["circle", "ellipse"] as const;
8
+ const BACKGROUND_KEYS = new Set(["color", "layers"]);
9
+ const LINEAR_KEYS = new Set(["type", "stops", "angle", "blend", "opacity"]);
10
+ const RADIAL_KEYS = new Set([
11
+ "type",
12
+ "stops",
13
+ "shape",
14
+ "position",
15
+ "blend",
16
+ "opacity",
17
+ ]);
18
+ const STOP_KEYS = new Set(["color", "at"]);
19
+ const POSITION_KEYS = new Set(["x", "y"]);
20
+ const LAYER_TYPES = "linear, radial";
21
+
22
+ function isRecord(value: unknown): value is IndexedRecord {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ function isNonEmptyString(value: unknown): value is string {
27
+ return typeof value === "string" && value.trim().length > 0;
28
+ }
29
+
30
+ function isFiniteInRange(value: unknown, min: number, max: number): boolean {
31
+ return (
32
+ typeof value === "number" &&
33
+ Number.isFinite(value) &&
34
+ value >= min &&
35
+ value <= max
36
+ );
37
+ }
38
+
39
+ function typeIssue(
40
+ path: string,
41
+ message: string,
42
+ expected: string,
43
+ received: unknown,
44
+ ): ValidationIssue {
45
+ return issue(path, "wrong_type", message, {
46
+ expected,
47
+ received: describeType(received),
48
+ });
49
+ }
50
+
51
+ function requiredIssue(path: string, message: string): ValidationIssue {
52
+ return issue(path, "required", message, {
53
+ suggestion: `Provide a value for \`${path}\`.`,
54
+ });
55
+ }
56
+
57
+ function enumIssue(
58
+ path: string,
59
+ message: string,
60
+ allowed: readonly string[],
61
+ received: unknown,
62
+ ): ValidationIssue {
63
+ return issue(path, "invalid_enum", message, {
64
+ expected: allowed.join(", "),
65
+ received: typeof received === "string" ? received : describeType(received),
66
+ suggestion: `Use one of: ${allowed.join(", ")}.`,
67
+ });
68
+ }
69
+
70
+ function valueIssue(
71
+ path: string,
72
+ message: string,
73
+ received: unknown,
74
+ ): ValidationIssue {
75
+ return issue(path, "invalid_value", message, {
76
+ received:
77
+ typeof received === "number" ? String(received) : describeType(received),
78
+ });
79
+ }
80
+
81
+ function unknownFields(
82
+ node: IndexedRecord,
83
+ allowed: ReadonlySet<string>,
84
+ path: string,
85
+ ): readonly ValidationIssue[] {
86
+ return Object.keys(node)
87
+ .filter((key) => !allowed.has(key))
88
+ .map((key) =>
89
+ issue(
90
+ `${path}.${key}`,
91
+ "unknown_field",
92
+ `${path}.${key} is not allowed`,
93
+ {
94
+ expected: [...allowed].join(", "),
95
+ suggestion: `Remove \`${key}\`; it is not a recognized field here.`,
96
+ },
97
+ ),
98
+ );
99
+ }
100
+
101
+ function validateStop(
102
+ value: unknown,
103
+ path: string,
104
+ ): readonly ValidationIssue[] {
105
+ if (!isRecord(value)) {
106
+ return [typeIssue(path, `${path} must be an object`, "object", value)];
107
+ }
108
+ const issues: ValidationIssue[] = [];
109
+ issues.push(...unknownFields(value, STOP_KEYS, path));
110
+ if (!isNonEmptyString(value.color)) {
111
+ issues.push(requiredIssue(`${path}.color`, `${path}.color is required`));
112
+ }
113
+ if (value.at !== undefined && !isFiniteInRange(value.at, 0, 100)) {
114
+ issues.push(
115
+ valueIssue(
116
+ `${path}.at`,
117
+ `${path}.at must be a percentage 0-100`,
118
+ value.at,
119
+ ),
120
+ );
121
+ }
122
+ return issues;
123
+ }
124
+
125
+ function validateStops(
126
+ value: unknown,
127
+ path: string,
128
+ ): readonly ValidationIssue[] {
129
+ if (!Array.isArray(value)) {
130
+ if (value === undefined) {
131
+ return [requiredIssue(path, `${path} is required`)];
132
+ }
133
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
134
+ }
135
+ if (value.length === 0) {
136
+ return [
137
+ issue(path, "required", `${path} must have at least one stop`, {
138
+ suggestion: "A gradient needs one or more color stops.",
139
+ }),
140
+ ];
141
+ }
142
+ const issues: ValidationIssue[] = [];
143
+ value.forEach((stop, index) => {
144
+ issues.push(...validateStop(stop, `${path}[${index}]`));
145
+ });
146
+ return issues;
147
+ }
148
+
149
+ function validateCompositing(
150
+ node: IndexedRecord,
151
+ path: string,
152
+ ): readonly ValidationIssue[] {
153
+ const issues: ValidationIssue[] = [];
154
+ if (
155
+ node.blend !== undefined &&
156
+ !(
157
+ typeof node.blend === "string" &&
158
+ BLEND_MODES.includes(node.blend as never)
159
+ )
160
+ ) {
161
+ issues.push(
162
+ enumIssue(
163
+ `${path}.blend`,
164
+ `${path}.blend must be normal, screen, multiply, or overlay`,
165
+ BLEND_MODES,
166
+ node.blend,
167
+ ),
168
+ );
169
+ }
170
+ if (node.opacity !== undefined && !isFiniteInRange(node.opacity, 0, 1)) {
171
+ issues.push(
172
+ valueIssue(
173
+ `${path}.opacity`,
174
+ `${path}.opacity must be a number 0-1`,
175
+ node.opacity,
176
+ ),
177
+ );
178
+ }
179
+ return issues;
180
+ }
181
+
182
+ function validateLayer(
183
+ value: unknown,
184
+ path: string,
185
+ ): readonly ValidationIssue[] {
186
+ if (!isRecord(value)) {
187
+ return [typeIssue(path, `${path} must be an object`, "object", value)];
188
+ }
189
+ const issues: ValidationIssue[] = [];
190
+
191
+ if (value.type === "linear") {
192
+ issues.push(...unknownFields(value, LINEAR_KEYS, path));
193
+ issues.push(...validateStops(value.stops, `${path}.stops`));
194
+ if (
195
+ value.angle !== undefined &&
196
+ !(typeof value.angle === "number" && Number.isFinite(value.angle))
197
+ ) {
198
+ issues.push(
199
+ valueIssue(
200
+ `${path}.angle`,
201
+ `${path}.angle must be a finite number`,
202
+ value.angle,
203
+ ),
204
+ );
205
+ }
206
+ issues.push(...validateCompositing(value, path));
207
+ } else if (value.type === "radial") {
208
+ issues.push(...unknownFields(value, RADIAL_KEYS, path));
209
+ issues.push(...validateStops(value.stops, `${path}.stops`));
210
+ if (
211
+ value.shape !== undefined &&
212
+ !(
213
+ typeof value.shape === "string" &&
214
+ SHAPE_VALUES.includes(value.shape as never)
215
+ )
216
+ ) {
217
+ issues.push(
218
+ enumIssue(
219
+ `${path}.shape`,
220
+ `${path}.shape must be circle or ellipse`,
221
+ SHAPE_VALUES,
222
+ value.shape,
223
+ ),
224
+ );
225
+ }
226
+ if (value.position !== undefined) {
227
+ if (!isRecord(value.position)) {
228
+ issues.push(
229
+ typeIssue(
230
+ `${path}.position`,
231
+ `${path}.position must be an object`,
232
+ "object",
233
+ value.position,
234
+ ),
235
+ );
236
+ } else {
237
+ issues.push(
238
+ ...unknownFields(value.position, POSITION_KEYS, `${path}.position`),
239
+ );
240
+ for (const axis of ["x", "y"] as const) {
241
+ if (!isFiniteInRange(value.position[axis], 0, 100)) {
242
+ issues.push(
243
+ valueIssue(
244
+ `${path}.position.${axis}`,
245
+ `${path}.position.${axis} must be a percentage 0-100`,
246
+ value.position[axis],
247
+ ),
248
+ );
249
+ }
250
+ }
251
+ }
252
+ }
253
+ issues.push(...validateCompositing(value, path));
254
+ } else {
255
+ issues.push(
256
+ issue(
257
+ `${path}.type`,
258
+ "unknown_kind",
259
+ `${path}.type must be a known gradient type`,
260
+ {
261
+ expected: LAYER_TYPES,
262
+ received:
263
+ typeof value.type === "string"
264
+ ? value.type
265
+ : describeType(value.type),
266
+ suggestion: `Use one of: ${LAYER_TYPES}.`,
267
+ },
268
+ ),
269
+ );
270
+ }
271
+ return issues;
272
+ }
273
+
274
+ export function validateBackground(
275
+ value: unknown,
276
+ path: string,
277
+ ): readonly ValidationIssue[] {
278
+ if (!isRecord(value)) {
279
+ return [typeIssue(path, `${path} must be an object`, "object", value)];
280
+ }
281
+ const issues: ValidationIssue[] = [];
282
+ issues.push(...unknownFields(value, BACKGROUND_KEYS, path));
283
+
284
+ if (value.color !== undefined && !isNonEmptyString(value.color)) {
285
+ issues.push(
286
+ value.color === ""
287
+ ? requiredIssue(`${path}.color`, `${path}.color must not be empty`)
288
+ : typeIssue(
289
+ `${path}.color`,
290
+ `${path}.color must be a string`,
291
+ "string",
292
+ value.color,
293
+ ),
294
+ );
295
+ }
296
+
297
+ if (value.layers !== undefined) {
298
+ if (!Array.isArray(value.layers)) {
299
+ issues.push(
300
+ typeIssue(
301
+ `${path}.layers`,
302
+ `${path}.layers must be an array`,
303
+ "array",
304
+ value.layers,
305
+ ),
306
+ );
307
+ } else {
308
+ value.layers.forEach((layer, index) => {
309
+ issues.push(...validateLayer(layer, `${path}.layers[${index}]`));
310
+ });
311
+ }
312
+ }
313
+
314
+ return issues;
315
+ }