@mseep/dembrandt 0.19.5

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +532 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/browser.d.ts +16 -0
  7. package/dist/lib/browser.js +27 -0
  8. package/dist/lib/browser.js.map +1 -0
  9. package/dist/lib/colors.d.ts +101 -0
  10. package/dist/lib/colors.js +405 -0
  11. package/dist/lib/colors.js.map +1 -0
  12. package/dist/lib/compare.d.ts +31 -0
  13. package/dist/lib/compare.js +46 -0
  14. package/dist/lib/compare.js.map +1 -0
  15. package/dist/lib/discovery.d.ts +31 -0
  16. package/dist/lib/discovery.js +243 -0
  17. package/dist/lib/discovery.js.map +1 -0
  18. package/dist/lib/drift.d.ts +64 -0
  19. package/dist/lib/drift.js +383 -0
  20. package/dist/lib/drift.js.map +1 -0
  21. package/dist/lib/dtcg/validate.d.ts +51 -0
  22. package/dist/lib/dtcg/validate.js +1403 -0
  23. package/dist/lib/dtcg/validate.js.map +1 -0
  24. package/dist/lib/exit-codes.d.ts +29 -0
  25. package/dist/lib/exit-codes.js +26 -0
  26. package/dist/lib/exit-codes.js.map +1 -0
  27. package/dist/lib/extractors/breakpoints.d.ts +5 -0
  28. package/dist/lib/extractors/breakpoints.js +450 -0
  29. package/dist/lib/extractors/breakpoints.js.map +1 -0
  30. package/dist/lib/extractors/colors.d.ts +2 -0
  31. package/dist/lib/extractors/colors.js +657 -0
  32. package/dist/lib/extractors/colors.js.map +1 -0
  33. package/dist/lib/extractors/components.d.ts +4 -0
  34. package/dist/lib/extractors/components.js +370 -0
  35. package/dist/lib/extractors/components.js.map +1 -0
  36. package/dist/lib/extractors/index.d.ts +9 -0
  37. package/dist/lib/extractors/index.js +1257 -0
  38. package/dist/lib/extractors/index.js.map +1 -0
  39. package/dist/lib/extractors/logo.d.ts +2 -0
  40. package/dist/lib/extractors/logo.js +626 -0
  41. package/dist/lib/extractors/logo.js.map +1 -0
  42. package/dist/lib/extractors/spacing.d.ts +4 -0
  43. package/dist/lib/extractors/spacing.js +163 -0
  44. package/dist/lib/extractors/spacing.js.map +1 -0
  45. package/dist/lib/extractors/teach.d.ts +1 -0
  46. package/dist/lib/extractors/teach.js +66 -0
  47. package/dist/lib/extractors/teach.js.map +1 -0
  48. package/dist/lib/extractors/typography.d.ts +1 -0
  49. package/dist/lib/extractors/typography.js +163 -0
  50. package/dist/lib/extractors/typography.js.map +1 -0
  51. package/dist/lib/findings.d.ts +34 -0
  52. package/dist/lib/findings.js +166 -0
  53. package/dist/lib/findings.js.map +1 -0
  54. package/dist/lib/formatters/dtcg.d.ts +10 -0
  55. package/dist/lib/formatters/dtcg.js +416 -0
  56. package/dist/lib/formatters/dtcg.js.map +1 -0
  57. package/dist/lib/formatters/html.d.ts +25 -0
  58. package/dist/lib/formatters/html.js +479 -0
  59. package/dist/lib/formatters/html.js.map +1 -0
  60. package/dist/lib/formatters/markdown.d.ts +5 -0
  61. package/dist/lib/formatters/markdown.js +568 -0
  62. package/dist/lib/formatters/markdown.js.map +1 -0
  63. package/dist/lib/formatters/pdf.d.ts +12 -0
  64. package/dist/lib/formatters/pdf.js +1121 -0
  65. package/dist/lib/formatters/pdf.js.map +1 -0
  66. package/dist/lib/formatters/terminal.d.ts +6 -0
  67. package/dist/lib/formatters/terminal.js +954 -0
  68. package/dist/lib/formatters/terminal.js.map +1 -0
  69. package/dist/lib/formatters/theme.d.ts +35 -0
  70. package/dist/lib/formatters/theme.js +37 -0
  71. package/dist/lib/formatters/theme.js.map +1 -0
  72. package/dist/lib/merger.d.ts +14 -0
  73. package/dist/lib/merger.js +362 -0
  74. package/dist/lib/merger.js.map +1 -0
  75. package/dist/lib/normalize.d.ts +29 -0
  76. package/dist/lib/normalize.js +59 -0
  77. package/dist/lib/normalize.js.map +1 -0
  78. package/dist/lib/robots.d.ts +12 -0
  79. package/dist/lib/robots.js +110 -0
  80. package/dist/lib/robots.js.map +1 -0
  81. package/dist/lib/run-summary.d.ts +40 -0
  82. package/dist/lib/run-summary.js +64 -0
  83. package/dist/lib/run-summary.js.map +1 -0
  84. package/dist/lib/types.d.ts +329 -0
  85. package/dist/lib/types.js +7 -0
  86. package/dist/lib/types.js.map +1 -0
  87. package/dist/lib/version.d.ts +134 -0
  88. package/dist/lib/version.js +153 -0
  89. package/dist/lib/version.js.map +1 -0
  90. package/dist/mcp-server.d.ts +11 -0
  91. package/dist/mcp-server.js +311 -0
  92. package/dist/mcp-server.js.map +1 -0
  93. package/dist/package.json +106 -0
  94. package/dist/test/_vitest-shim.d.ts +13 -0
  95. package/dist/test/_vitest-shim.js +23 -0
  96. package/dist/test/_vitest-shim.js.map +1 -0
  97. package/dist/test/cli.test.d.ts +1 -0
  98. package/dist/test/cli.test.js +24 -0
  99. package/dist/test/cli.test.js.map +1 -0
  100. package/dist/test/colors.test.d.ts +1 -0
  101. package/dist/test/colors.test.js +64 -0
  102. package/dist/test/colors.test.js.map +1 -0
  103. package/dist/test/compare.test.d.ts +1 -0
  104. package/dist/test/compare.test.js +57 -0
  105. package/dist/test/compare.test.js.map +1 -0
  106. package/dist/test/drift.test.d.ts +1 -0
  107. package/dist/test/drift.test.js +53 -0
  108. package/dist/test/drift.test.js.map +1 -0
  109. package/dist/test/dtcg-formatter.test.d.ts +1 -0
  110. package/dist/test/dtcg-formatter.test.js +48 -0
  111. package/dist/test/dtcg-formatter.test.js.map +1 -0
  112. package/dist/test/dtcg-validate.test.d.ts +1 -0
  113. package/dist/test/dtcg-validate.test.js +2129 -0
  114. package/dist/test/dtcg-validate.test.js.map +1 -0
  115. package/dist/test/exit-codes.test.d.ts +1 -0
  116. package/dist/test/exit-codes.test.js +53 -0
  117. package/dist/test/exit-codes.test.js.map +1 -0
  118. package/dist/test/findings.test.d.ts +1 -0
  119. package/dist/test/findings.test.js +77 -0
  120. package/dist/test/findings.test.js.map +1 -0
  121. package/dist/test/html.test.d.ts +1 -0
  122. package/dist/test/html.test.js +95 -0
  123. package/dist/test/html.test.js.map +1 -0
  124. package/dist/test/markdown.test.d.ts +1 -0
  125. package/dist/test/markdown.test.js +145 -0
  126. package/dist/test/markdown.test.js.map +1 -0
  127. package/dist/test/merger.test.d.ts +1 -0
  128. package/dist/test/merger.test.js +98 -0
  129. package/dist/test/merger.test.js.map +1 -0
  130. package/dist/test/normalize.test.d.ts +1 -0
  131. package/dist/test/normalize.test.js +47 -0
  132. package/dist/test/normalize.test.js.map +1 -0
  133. package/dist/test/run-summary.test.d.ts +1 -0
  134. package/dist/test/run-summary.test.js +45 -0
  135. package/dist/test/run-summary.test.js.map +1 -0
  136. package/dist/test/version.test.d.ts +1 -0
  137. package/dist/test/version.test.js +73 -0
  138. package/dist/test/version.test.js.map +1 -0
  139. package/package.json +106 -0
@@ -0,0 +1,1403 @@
1
+ /**
2
+ * W3C Design Tokens Community Group (DTCG) Validator
3
+ * Validates design tokens against the W3C DTCG specification
4
+ * @see https://www.designtokens.org/TR/2025.10/format/
5
+ */
6
+ /**
7
+ * Valid token types according to W3C DTCG spec
8
+ */
9
+ const VALID_TOKEN_TYPES = [
10
+ 'color',
11
+ 'dimension',
12
+ 'fontFamily',
13
+ 'fontWeight',
14
+ 'duration',
15
+ 'cubicBezier',
16
+ 'number',
17
+ 'strokeStyle',
18
+ 'border',
19
+ 'transition',
20
+ 'shadow',
21
+ 'gradient',
22
+ 'typography'
23
+ ];
24
+ /**
25
+ * Valid font weight string aliases
26
+ */
27
+ const FONT_WEIGHT_ALIASES = {
28
+ 'thin': 100,
29
+ 'hairline': 100,
30
+ 'extra-light': 200,
31
+ 'ultra-light': 200,
32
+ 'light': 300,
33
+ 'normal': 400,
34
+ 'regular': 400,
35
+ 'book': 400,
36
+ 'medium': 500,
37
+ 'semi-bold': 600,
38
+ 'demi-bold': 600,
39
+ 'bold': 700,
40
+ 'extra-bold': 800,
41
+ 'ultra-bold': 800,
42
+ 'black': 900,
43
+ 'heavy': 900,
44
+ 'extra-black': 950,
45
+ 'ultra-black': 950
46
+ };
47
+ /**
48
+ * Valid stroke style string values
49
+ */
50
+ const STROKE_STYLE_VALUES = [
51
+ 'solid',
52
+ 'dashed',
53
+ 'dotted',
54
+ 'double',
55
+ 'groove',
56
+ 'ridge',
57
+ 'outset',
58
+ 'inset'
59
+ ];
60
+ /**
61
+ * Valid color spaces and their component requirements
62
+ * Per W3C Design Tokens Color Module 2025.10
63
+ */
64
+ const COLOR_SPACES = {
65
+ 'srgb': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
66
+ 'srgb-linear': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
67
+ 'hsl': { components: 3, ranges: [[0, 360], [0, 100], [0, 100]] },
68
+ 'hwb': { components: 3, ranges: [[0, 360], [0, 100], [0, 100]] },
69
+ 'lab': { components: 3, ranges: [[0, 100], [-Infinity, Infinity], [-Infinity, Infinity]] },
70
+ 'lch': { components: 3, ranges: [[0, 100], [0, Infinity], [0, 360]] },
71
+ 'oklab': { components: 3, ranges: [[0, 1], [-Infinity, Infinity], [-Infinity, Infinity]] },
72
+ 'oklch': { components: 3, ranges: [[0, 1], [0, Infinity], [0, 360]] },
73
+ 'display-p3': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
74
+ 'a98-rgb': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
75
+ 'prophoto-rgb': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
76
+ 'rec2020': { components: 3, ranges: [[0, 1], [0, 1], [0, 1]] },
77
+ // Spec table lists XYZ as [0,1], but the real D65/D50 reference whites exceed 1
78
+ // (e.g. D65 white Z ≈ 1.089). Treating XYZ as effectively unbounded avoids
79
+ // rejecting physically valid colors. See Color Module 4.2.13 / 4.2.14.
80
+ 'xyz-d65': { components: 3, ranges: [[-Infinity, Infinity], [-Infinity, Infinity], [-Infinity, Infinity]] },
81
+ 'xyz-d50': { components: 3, ranges: [[-Infinity, Infinity], [-Infinity, Infinity], [-Infinity, Infinity]] }
82
+ };
83
+ /**
84
+ * Validates a color value per W3C Design Tokens Color Module
85
+ * Color can be:
86
+ * - String hex format: #rrggbb (no alpha in hex per spec)
87
+ * - Object format with colorSpace, components, optional alpha and hex
88
+ * - Reference: {token.path}
89
+ */
90
+ function validateColorValue(value, path, errors, _warnings) {
91
+ if (typeof value === 'string') {
92
+ // Per Color Module §4.1, a color $value MUST be an object (colorSpace +
93
+ // components) or a reference. Bare strings such as "#0066cc" are not valid.
94
+ if (!isReference(value)) {
95
+ errors.push(`Color at ${path} must be an object with colorSpace and components, or a reference. Bare strings like "#0066cc" are not valid color values (Color Module §4.1).`);
96
+ }
97
+ }
98
+ else if (typeof value === 'object' && value !== null) {
99
+ // W3C color object format
100
+ if (!value.colorSpace) {
101
+ errors.push(`Color object at ${path} must have colorSpace property`);
102
+ return;
103
+ }
104
+ // Validate color space is supported
105
+ const colorSpaceInfo = COLOR_SPACES[value.colorSpace];
106
+ if (!colorSpaceInfo) {
107
+ errors.push(`Color at ${path} has unsupported colorSpace "${value.colorSpace}". Supported: ${Object.keys(COLOR_SPACES).join(', ')}`);
108
+ return;
109
+ }
110
+ // Validate components array
111
+ if (!Array.isArray(value.components)) {
112
+ errors.push(`Color object at ${path} must have components array`);
113
+ return;
114
+ }
115
+ if (value.components.length !== colorSpaceInfo.components) {
116
+ errors.push(`Color object at ${path} must have components array with exactly ${colorSpaceInfo.components} values`);
117
+ return;
118
+ }
119
+ // Validate each component
120
+ value.components.forEach((component, idx) => {
121
+ // A component may be a property-level JSON Pointer reference ($ref) per §7.3.1.
122
+ if (isPointerRef(component)) {
123
+ const resolved = resolvePointerDeep(component.$ref);
124
+ if (resolved.error) {
125
+ errors.push(`${resolved.error} at ${path}.components[${idx}]`);
126
+ }
127
+ else if (resolved.value !== 'none' && typeof resolved.value !== 'number') {
128
+ errors.push(`Color component reference at ${path}.components[${idx}] must resolve to a number or "none"`);
129
+ }
130
+ return;
131
+ }
132
+ // Components can be numbers or "none" keyword
133
+ if (component !== 'none' && typeof component !== 'number') {
134
+ errors.push(`Color component at ${path}.components[${idx}] must be a number or "none"`);
135
+ return;
136
+ }
137
+ // Validate numeric component ranges
138
+ if (typeof component === 'number') {
139
+ const [min, max] = colorSpaceInfo.ranges[idx];
140
+ // Check if this is a hue component (0-360 exclusive)
141
+ const isHueComponent = (value.colorSpace === 'hsl' && idx === 0) ||
142
+ (value.colorSpace === 'hwb' && idx === 0) ||
143
+ (value.colorSpace === 'lch' && idx === 2) ||
144
+ (value.colorSpace === 'oklch' && idx === 2);
145
+ if (isHueComponent) {
146
+ // Hue component (0-360 exclusive)
147
+ if (component < 0 || component >= 360) {
148
+ errors.push(`Color hue component at ${path}.components[${idx}] must be >= 0 and < 360`);
149
+ }
150
+ }
151
+ else if (min !== -Infinity && max !== Infinity) {
152
+ // Other bounded components
153
+ if (component < min || component > max) {
154
+ errors.push(`Color component at ${path}.components[${idx}] must be between ${min} and ${max}`);
155
+ }
156
+ }
157
+ else if (min !== -Infinity && max === Infinity) {
158
+ // Chroma-like components (>= 0)
159
+ if (component < min) {
160
+ errors.push(`Color component at ${path}.components[${idx}] must be >= ${min}`);
161
+ }
162
+ }
163
+ // Unbounded components (like a, b in lab) - no validation needed
164
+ }
165
+ });
166
+ // hex property is optional but should be 6-digit string if present
167
+ if (value.hex !== undefined) {
168
+ if (typeof value.hex !== 'string') {
169
+ errors.push(`Color hex property at ${path} must be a string`);
170
+ }
171
+ else if (!value.hex.match(/^#[0-9a-fA-F]{6}$/)) {
172
+ errors.push(`Color hex property at ${path} must be 6-digit hex format (#rrggbb)`);
173
+ }
174
+ }
175
+ // alpha property is optional but must be [0-1] if present
176
+ if (value.alpha !== undefined) {
177
+ if (typeof value.alpha !== 'number') {
178
+ errors.push(`Color alpha property at ${path} must be a number`);
179
+ }
180
+ else if (value.alpha < 0 || value.alpha > 1) {
181
+ errors.push(`Color alpha property at ${path} must be between 0 and 1`);
182
+ }
183
+ }
184
+ }
185
+ else {
186
+ errors.push(`Color at ${path} must be a string or object`);
187
+ }
188
+ }
189
+ /**
190
+ * Validates a dimension value
191
+ * Must have unit "px" or "rem" per spec
192
+ */
193
+ function validateDimensionValue(value, path, errors) {
194
+ // References are allowed wherever a dimension value is expected.
195
+ if (isReference(value))
196
+ return;
197
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
198
+ // Object format: { value: number, unit: "px" | "rem" } — the only valid form per §8.2.
199
+ // Each of value/unit may be a property-level JSON Pointer reference ($ref) per §7.3.2.
200
+ if (isPointerRef(value.value)) {
201
+ const r = resolvePointerDeep(value.value.$ref);
202
+ if (r.error)
203
+ errors.push(`${r.error} at ${path}.value`);
204
+ else if (typeof r.value !== 'number')
205
+ errors.push(`Dimension value reference at ${path}.value must resolve to a number`);
206
+ }
207
+ else if (typeof value.value !== 'number') {
208
+ errors.push(`Dimension object at ${path} must have numeric value property`);
209
+ }
210
+ if (isPointerRef(value.unit)) {
211
+ const r = resolvePointerDeep(value.unit.$ref);
212
+ if (r.error)
213
+ errors.push(`${r.error} at ${path}.unit`);
214
+ else if (r.value !== 'px' && r.value !== 'rem')
215
+ errors.push(`Dimension unit reference at ${path}.unit must resolve to "px" or "rem"`);
216
+ }
217
+ else if (value.unit !== 'px' && value.unit !== 'rem') {
218
+ errors.push(`Dimension unit at ${path} must be "px" or "rem"`);
219
+ }
220
+ }
221
+ else {
222
+ errors.push(`Dimension at ${path} must be an object { value, unit } with unit "px" or "rem", or a reference. Bare numbers and strings like "8px" are not valid (§8.2).`);
223
+ }
224
+ }
225
+ /**
226
+ * Validates a duration value
227
+ * Must have unit "ms" or "s"
228
+ */
229
+ function validateDurationValue(value, path, errors) {
230
+ // References are allowed wherever a duration value is expected.
231
+ if (isReference(value))
232
+ return;
233
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
234
+ // Each of value/unit may be a property-level JSON Pointer reference ($ref) per §7.3.2.
235
+ if (isPointerRef(value.value)) {
236
+ const r = resolvePointerDeep(value.value.$ref);
237
+ if (r.error)
238
+ errors.push(`${r.error} at ${path}.value`);
239
+ else if (typeof r.value !== 'number')
240
+ errors.push(`Duration value reference at ${path}.value must resolve to a number`);
241
+ }
242
+ else if (typeof value.value !== 'number') {
243
+ errors.push(`Duration object at ${path} must have numeric value property`);
244
+ }
245
+ if (isPointerRef(value.unit)) {
246
+ const r = resolvePointerDeep(value.unit.$ref);
247
+ if (r.error)
248
+ errors.push(`${r.error} at ${path}.unit`);
249
+ else if (r.value !== 'ms' && r.value !== 's')
250
+ errors.push(`Duration unit reference at ${path}.unit must resolve to "ms" or "s"`);
251
+ }
252
+ else if (value.unit !== 'ms' && value.unit !== 's') {
253
+ errors.push(`Duration unit at ${path} must be "ms" or "s"`);
254
+ }
255
+ }
256
+ else {
257
+ errors.push(`Duration at ${path} must be an object { value, unit } with unit "ms" or "s", or a reference.`);
258
+ }
259
+ }
260
+ /**
261
+ * Validates a cubic bezier value
262
+ * Must be array of 4 numbers: [P1x, P1y, P2x, P2y]
263
+ * P1x and P2x must be in range [0, 1]
264
+ */
265
+ function validateCubicBezierValue(value, path, errors) {
266
+ if (!Array.isArray(value) || value.length !== 4) {
267
+ errors.push(`cubicBezier at ${path} must be an array of exactly 4 numbers`);
268
+ return;
269
+ }
270
+ value.forEach((num, idx) => {
271
+ if (typeof num !== 'number') {
272
+ errors.push(`cubicBezier at ${path}[${idx}] must be a number`);
273
+ }
274
+ });
275
+ // X coordinates (indices 0 and 2) must be in [0, 1]
276
+ if (typeof value[0] === 'number' && (value[0] < 0 || value[0] > 1)) {
277
+ errors.push(`cubicBezier at ${path}[0] (P1x) must be in range [0, 1]`);
278
+ }
279
+ if (typeof value[2] === 'number' && (value[2] < 0 || value[2] > 1)) {
280
+ errors.push(`cubicBezier at ${path}[2] (P2x) must be in range [0, 1]`);
281
+ }
282
+ }
283
+ /**
284
+ * Validates a stroke style value
285
+ * Can be string enum or object with dashArray and lineCap
286
+ */
287
+ function validateStrokeStyleValue(value, path, errors, warnings) {
288
+ if (isReference(value))
289
+ return;
290
+ if (typeof value === 'string') {
291
+ if (!STROKE_STYLE_VALUES.includes(value)) {
292
+ errors.push(`strokeStyle at ${path} must be one of: ${STROKE_STYLE_VALUES.join(', ')}`);
293
+ }
294
+ }
295
+ else if (typeof value === 'object' && value !== null) {
296
+ // Object format: { dashArray: dimension[], lineCap: "round"|"butt"|"square" }
297
+ if (!value.dashArray) {
298
+ errors.push(`strokeStyle object at ${path} must have dashArray property`);
299
+ }
300
+ else if (!Array.isArray(value.dashArray)) {
301
+ errors.push(`strokeStyle dashArray at ${path} must be an array`);
302
+ }
303
+ else {
304
+ // Each dash entry must be a dimension value or a reference to a dimension token
305
+ value.dashArray.forEach((d, i) => validateSubValue(d, 'dimension', `${path}.dashArray[${i}]`, errors, warnings));
306
+ }
307
+ if (!value.lineCap) {
308
+ errors.push(`strokeStyle object at ${path} must have lineCap property`);
309
+ }
310
+ else if (!['round', 'butt', 'square'].includes(value.lineCap)) {
311
+ errors.push(`strokeStyle lineCap at ${path} must be "round", "butt", or "square"`);
312
+ }
313
+ }
314
+ else {
315
+ errors.push(`strokeStyle at ${path} must be a string or object`);
316
+ }
317
+ }
318
+ /**
319
+ * Validates a border value
320
+ * Must have color, width, and style properties
321
+ */
322
+ function validateBorderValue(value, path, errors, warnings) {
323
+ if (isReference(value))
324
+ return;
325
+ if (typeof value !== 'object' || value === null) {
326
+ errors.push(`Border at ${path} must be an object`);
327
+ return;
328
+ }
329
+ if (!('color' in value)) {
330
+ errors.push(`Border at ${path} must have color property`);
331
+ }
332
+ else {
333
+ validateSubValue(value.color, 'color', `${path}.color`, errors, warnings);
334
+ }
335
+ if (!('width' in value)) {
336
+ errors.push(`Border at ${path} must have width property`);
337
+ }
338
+ else {
339
+ validateSubValue(value.width, 'dimension', `${path}.width`, errors, warnings);
340
+ }
341
+ if (!('style' in value)) {
342
+ errors.push(`Border at ${path} must have style property`);
343
+ }
344
+ else {
345
+ validateSubValue(value.style, 'strokeStyle', `${path}.style`, errors, warnings);
346
+ }
347
+ }
348
+ /**
349
+ * Validates a transition value
350
+ * Must have duration, delay, and timingFunction properties
351
+ */
352
+ function validateTransitionValue(value, path, errors, warnings) {
353
+ if (isReference(value))
354
+ return;
355
+ if (typeof value !== 'object' || value === null) {
356
+ errors.push(`Transition at ${path} must be an object`);
357
+ return;
358
+ }
359
+ const fields = [['duration', 'duration'], ['delay', 'duration'], ['timingFunction', 'cubicBezier']];
360
+ for (const [field, subType] of fields) {
361
+ if (!(field in value)) {
362
+ errors.push(`Transition at ${path} must have ${field} property`);
363
+ }
364
+ else {
365
+ validateSubValue(value[field], subType, `${path}.${field}`, errors, warnings);
366
+ }
367
+ }
368
+ }
369
+ /**
370
+ * Validates a shadow value
371
+ * Can be single shadow object or array of shadow objects
372
+ * Each shadow must have: color, offsetX, offsetY, blur, spread
373
+ * Optional: inset (boolean)
374
+ */
375
+ function validateShadowValue(value, path, errors, warnings) {
376
+ const subFields = [['color', 'color'], ['offsetX', 'dimension'], ['offsetY', 'dimension'], ['blur', 'dimension'], ['spread', 'dimension']];
377
+ const validateSingleShadow = (shadow, shadowPath) => {
378
+ // A shadow array element (or the single value) may be a reference to a shadow token.
379
+ if (isReference(shadow))
380
+ return;
381
+ if (typeof shadow !== 'object' || shadow === null) {
382
+ errors.push(`Shadow at ${shadowPath} must be an object or a reference`);
383
+ return;
384
+ }
385
+ for (const [field, subType] of subFields) {
386
+ if (!(field in shadow)) {
387
+ errors.push(`Shadow at ${shadowPath} is missing required field: ${field}`);
388
+ }
389
+ else {
390
+ validateSubValue(shadow[field], subType, `${shadowPath}.${field}`, errors, warnings);
391
+ }
392
+ }
393
+ // inset is optional but must be boolean if present
394
+ if (shadow.inset !== undefined && typeof shadow.inset !== 'boolean') {
395
+ errors.push(`Shadow inset property at ${shadowPath} must be a boolean`);
396
+ }
397
+ };
398
+ if (isReference(value))
399
+ return;
400
+ if (Array.isArray(value)) {
401
+ value.forEach((shadow, idx) => {
402
+ validateSingleShadow(shadow, `${path}[${idx}]`);
403
+ });
404
+ }
405
+ else {
406
+ validateSingleShadow(value, path);
407
+ }
408
+ }
409
+ /**
410
+ * Validates a gradient value
411
+ * Must be array of gradient stops
412
+ * Each stop must have: color, position (number in [0, 1])
413
+ */
414
+ function validateGradientValue(value, path, errors, warnings) {
415
+ if (isReference(value))
416
+ return;
417
+ if (!Array.isArray(value)) {
418
+ errors.push(`Gradient at ${path} must be an array of gradient stops`);
419
+ return;
420
+ }
421
+ value.forEach((stop, idx) => {
422
+ const stopPath = `${path}[${idx}]`;
423
+ // An array element may be a reference to a gradient token.
424
+ if (isReference(stop))
425
+ return;
426
+ if (typeof stop !== 'object' || stop === null) {
427
+ errors.push(`Gradient stop at ${stopPath} must be an object or a reference`);
428
+ return;
429
+ }
430
+ if (!('color' in stop)) {
431
+ errors.push(`Gradient stop at ${stopPath} must have color property`);
432
+ }
433
+ else {
434
+ validateSubValue(stop.color, 'color', `${stopPath}.color`, errors, warnings);
435
+ }
436
+ // position is a number token; out-of-range values are clamped per §9.7, so any number is accepted.
437
+ if (!('position' in stop)) {
438
+ errors.push(`Gradient stop at ${stopPath} must have position property`);
439
+ }
440
+ else {
441
+ validateSubValue(stop.position, 'number', `${stopPath}.position`, errors, warnings);
442
+ }
443
+ });
444
+ }
445
+ /**
446
+ * Validates a typography value
447
+ * Must have: fontFamily, fontSize, fontWeight, letterSpacing, lineHeight
448
+ */
449
+ function validateTypographyValue(value, path, errors, warnings) {
450
+ if (isReference(value))
451
+ return;
452
+ if (typeof value !== 'object' || value === null) {
453
+ errors.push(`Typography at ${path} must be an object`);
454
+ return;
455
+ }
456
+ // Required fields per spec §9.8, each a value of the given type or a reference to such a token.
457
+ const fields = [
458
+ ['fontFamily', 'fontFamily'],
459
+ ['fontSize', 'dimension'],
460
+ ['fontWeight', 'fontWeight'],
461
+ ['letterSpacing', 'dimension'],
462
+ ['lineHeight', 'number'],
463
+ ];
464
+ for (const [field, subType] of fields) {
465
+ if (!(field in value)) {
466
+ errors.push(`Typography at ${path} is missing required field: ${field}`);
467
+ }
468
+ else {
469
+ validateSubValue(value[field], subType, `${path}.${field}`, errors, warnings);
470
+ }
471
+ }
472
+ // Warn about unknown fields
473
+ const allowedFields = fields.map((f) => f[0]);
474
+ for (const field of Object.keys(value)) {
475
+ if (!allowedFields.includes(field)) {
476
+ warnings.push(`Typography at ${path} has unknown field: ${field}`);
477
+ }
478
+ }
479
+ }
480
+ /**
481
+ * Validates a font weight value: a number in [1, 1000] or a predefined string alias.
482
+ */
483
+ function validateFontWeightValue(value, path, errors) {
484
+ if (typeof value === 'number') {
485
+ if (value < 1 || value > 1000) {
486
+ errors.push(`fontWeight at ${path} must be a number between 1-1000`);
487
+ }
488
+ }
489
+ else if (typeof value === 'string') {
490
+ if (!Object.prototype.hasOwnProperty.call(FONT_WEIGHT_ALIASES, value)) {
491
+ errors.push(`fontWeight at ${path} must be a valid weight alias (e.g., "bold", "normal") or a number between 1-1000`);
492
+ }
493
+ }
494
+ else {
495
+ errors.push(`fontWeight at ${path} must be a number or string`);
496
+ }
497
+ }
498
+ /**
499
+ * Validates a composite sub-value that must be of `expectedType` or a reference
500
+ * to a token of that type. Curly-brace references ({token.path}) are accepted
501
+ * here; deeper type-checking of references happens during reference resolution.
502
+ */
503
+ function validateSubValue(value, expectedType, path, errors, warnings) {
504
+ // Curly-brace token reference — accepted here; resolved elsewhere.
505
+ if (isReference(value))
506
+ return;
507
+ // Property-level JSON Pointer reference ($ref object): resolve and validate the target.
508
+ if (isPointerRef(value)) {
509
+ const resolved = resolvePointerDeep(value.$ref);
510
+ if (resolved.error) {
511
+ errors.push(`${resolved.error} at ${path}`);
512
+ return;
513
+ }
514
+ validateSubValue(resolved.value, expectedType, path, errors, warnings);
515
+ return;
516
+ }
517
+ switch (expectedType) {
518
+ case 'color':
519
+ validateColorValue(value, path, errors, warnings);
520
+ break;
521
+ case 'dimension':
522
+ validateDimensionValue(value, path, errors);
523
+ break;
524
+ case 'duration':
525
+ validateDurationValue(value, path, errors);
526
+ break;
527
+ case 'cubicBezier':
528
+ validateCubicBezierValue(value, path, errors);
529
+ break;
530
+ case 'strokeStyle':
531
+ validateStrokeStyleValue(value, path, errors, warnings);
532
+ break;
533
+ case 'fontWeight':
534
+ validateFontWeightValue(value, path, errors);
535
+ break;
536
+ case 'fontFamily':
537
+ if (typeof value !== 'string' && !Array.isArray(value)) {
538
+ errors.push(`fontFamily at ${path} must be a string or array`);
539
+ }
540
+ break;
541
+ case 'number':
542
+ if (typeof value !== 'number') {
543
+ errors.push(`number at ${path} must be a number`);
544
+ }
545
+ break;
546
+ }
547
+ }
548
+ /**
549
+ * Checks if a value is a reference (matches {path} syntax)
550
+ * Note: {path} must have at least one character inside
551
+ */
552
+ function isReference(value) {
553
+ return typeof value === 'string' && /^\{.+\}$/.test(value);
554
+ }
555
+ /**
556
+ * Extracts the path from a reference string
557
+ * e.g., "{color.primary}" => "color.primary"
558
+ */
559
+ function extractReferencePath(reference) {
560
+ return reference.replace(/^\{|\}$/g, '');
561
+ }
562
+ /**
563
+ * The parsed root document for the current validation run. JSON Pointer ($ref)
564
+ * references are resolved against this. Set at the start of validateTokens /
565
+ * validateTokensObject and cleared afterwards.
566
+ */
567
+ let _rootDoc = null;
568
+ /**
569
+ * True if a value is a property-level JSON Pointer reference object: { "$ref": "#/..." }.
570
+ */
571
+ function isPointerRef(value) {
572
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && typeof value.$ref === 'string';
573
+ }
574
+ /**
575
+ * Resolves an RFC 6901 JSON Pointer (e.g. "#/colors/blue/$value/components/0")
576
+ * against the current root document. Returns { value } or { error }.
577
+ */
578
+ function resolveJsonPointer(pointer) {
579
+ if (typeof pointer !== 'string' || pointer[0] !== '#') {
580
+ return { error: `JSON Pointer "${pointer}" must start with "#"` };
581
+ }
582
+ const frag = pointer.slice(1);
583
+ if (frag === '' || frag === '/') {
584
+ return { value: _rootDoc };
585
+ }
586
+ if (frag[0] !== '/') {
587
+ return { error: `JSON Pointer "${pointer}" must start with "#/"` };
588
+ }
589
+ // RFC 6901: split on "/", unescape ~1 -> "/" and ~0 -> "~"
590
+ const segments = frag.split('/').slice(1).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
591
+ let current = _rootDoc;
592
+ for (const seg of segments) {
593
+ if (current === null || current === undefined) {
594
+ return { error: `JSON Pointer "${pointer}" cannot be resolved (missing "${seg}")` };
595
+ }
596
+ if (Array.isArray(current)) {
597
+ const idx = Number(seg);
598
+ if (!Number.isInteger(idx) || idx < 0 || idx >= current.length) {
599
+ return { error: `JSON Pointer "${pointer}" has an invalid array index "${seg}"` };
600
+ }
601
+ current = current[idx];
602
+ }
603
+ else if (typeof current === 'object') {
604
+ if (!Object.prototype.hasOwnProperty.call(current, seg)) {
605
+ return { error: `JSON Pointer "${pointer}" cannot be resolved (no "${seg}")` };
606
+ }
607
+ current = current[seg];
608
+ }
609
+ else {
610
+ return { error: `JSON Pointer "${pointer}" cannot descend into primitive at "${seg}"` };
611
+ }
612
+ }
613
+ return { value: current };
614
+ }
615
+ /**
616
+ * Resolves a JSON Pointer and follows any further $ref / curly-brace references
617
+ * the target may itself be, with circular-reference detection.
618
+ * Returns { value } (fully resolved) or { error }.
619
+ */
620
+ function resolvePointerDeep(pointer, registry, visited = new Set()) {
621
+ if (visited.has(pointer)) {
622
+ return { error: `Circular $ref detected: ${Array.from(visited).join(' → ')} → ${pointer}` };
623
+ }
624
+ visited.add(pointer);
625
+ const result = resolveJsonPointer(pointer);
626
+ if (result.error)
627
+ return result;
628
+ let value = result.value;
629
+ // The target may itself be a pointer-ref object or a curly-brace reference.
630
+ if (isPointerRef(value)) {
631
+ return resolvePointerDeep(value.$ref, registry, visited);
632
+ }
633
+ if (isReference(value) && registry) {
634
+ const resolved = resolveReference(extractReferencePath(value), registry);
635
+ if (resolved.error)
636
+ return { error: resolved.error };
637
+ value = resolved.value;
638
+ }
639
+ return { value };
640
+ }
641
+ /**
642
+ * Reserved $-prefixed property keys defined by the spec. Any other $-prefixed
643
+ * key is an invalid token/group name (§5.1.1). $root is handled separately as
644
+ * it names a reserved root token.
645
+ */
646
+ const RESERVED_DOLLAR_KEYS = new Set([
647
+ '$value', '$type', '$description', '$extensions', '$deprecated', '$extends', '$ref', '$schema'
648
+ ]);
649
+ /**
650
+ * Validates the optional $deprecated property (token or group level): must be
651
+ * a boolean or a string explanation (§5.2.4 / §6.3.1).
652
+ */
653
+ function validateDeprecated(obj, path, errors) {
654
+ if ('$deprecated' in obj) {
655
+ const d = obj.$deprecated;
656
+ if (typeof d !== 'boolean' && typeof d !== 'string') {
657
+ errors.push(`$deprecated at ${path} must be a boolean or a string explanation`);
658
+ }
659
+ }
660
+ }
661
+ /**
662
+ * Resolves a group reference (used by $extends): a curly-brace "{group.path}"
663
+ * or a JSON Pointer "#/group/path". Returns { value } (the target node) or { error }.
664
+ */
665
+ function resolveGroupNode(ref) {
666
+ if (isReference(ref)) {
667
+ const segments = extractReferencePath(ref).split('.');
668
+ let node = _rootDoc;
669
+ for (const seg of segments) {
670
+ if (node && typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, seg)) {
671
+ node = node[seg];
672
+ }
673
+ else {
674
+ return { error: `$extends target "${ref}" cannot be resolved` };
675
+ }
676
+ }
677
+ return { value: node };
678
+ }
679
+ if (typeof ref === 'string' && ref[0] === '#') {
680
+ return resolveJsonPointer(ref);
681
+ }
682
+ return { error: `$extends must be a reference to a group` };
683
+ }
684
+ /**
685
+ * Validates a group's optional $extends inheritance (§6.4): the target must
686
+ * resolve, must be a group (not a token), and the chain must not be circular.
687
+ */
688
+ function validateExtends(group, path, errors) {
689
+ if (!('$extends' in group))
690
+ return;
691
+ const chain = new Set();
692
+ let node = group;
693
+ let label = path;
694
+ while (node && typeof node === 'object' && '$extends' in node) {
695
+ const ext = node.$extends;
696
+ if (typeof ext !== 'string' || !(isReference(ext) || ext[0] === '#')) {
697
+ errors.push(`$extends at ${label} must be a reference to a group`);
698
+ return;
699
+ }
700
+ const key = isReference(ext) ? extractReferencePath(ext) : ext;
701
+ if (key === path || chain.has(key)) {
702
+ errors.push(`Circular $extends detected involving ${path}`);
703
+ return;
704
+ }
705
+ chain.add(key);
706
+ const resolved = resolveGroupNode(ext);
707
+ if (resolved.error) {
708
+ errors.push(`${resolved.error} at ${label}`);
709
+ return;
710
+ }
711
+ const target = resolved.value;
712
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
713
+ errors.push(`$extends target "${ext}" at ${label} is not a group`);
714
+ return;
715
+ }
716
+ if ('$value' in target || '$ref' in target) {
717
+ errors.push(`$extends at ${label} must not reference a token (§6.4)`);
718
+ return;
719
+ }
720
+ node = target;
721
+ label = key;
722
+ }
723
+ }
724
+ /**
725
+ * Builds a registry of all tokens in the document
726
+ * Returns a Map where keys are paths and values are token objects
727
+ */
728
+ function buildTokenRegistry(obj, path = '', registry = new Map(), parentType = null) {
729
+ for (const [key, value] of Object.entries(obj)) {
730
+ if (key.startsWith('$') && key !== '$root')
731
+ continue;
732
+ const currentPath = path ? `${path}.${key}` : key;
733
+ if (value && typeof value === 'object') {
734
+ if ('$value' in value) {
735
+ // This is a token - add to registry
736
+ const tokenType = value.$type || parentType;
737
+ registry.set(currentPath, {
738
+ ...value,
739
+ $type: tokenType,
740
+ $path: currentPath
741
+ });
742
+ }
743
+ else if ('$ref' in value && typeof value.$ref === 'string') {
744
+ // Token defined by a JSON Pointer $ref — store the resolved value so that
745
+ // curly-brace references to this token resolve correctly.
746
+ const resolved = resolveJsonPointer(value.$ref);
747
+ registry.set(currentPath, {
748
+ ...value,
749
+ $value: resolved.error ? undefined : resolved.value,
750
+ $type: value.$type || parentType,
751
+ $path: currentPath
752
+ });
753
+ }
754
+ else {
755
+ // This is a group - check for group-level $type
756
+ const groupType = value.$type || parentType;
757
+ buildTokenRegistry(value, currentPath, registry, groupType);
758
+ }
759
+ }
760
+ }
761
+ return registry;
762
+ }
763
+ /**
764
+ * Resolves a reference to its final value
765
+ * Returns { value, type, error } where error is set if resolution failed
766
+ */
767
+ function resolveReference(referencePath, registry, visitedPaths = new Set()) {
768
+ // Check for circular references
769
+ if (visitedPaths.has(referencePath)) {
770
+ return {
771
+ error: `Circular reference detected: ${Array.from(visitedPaths).join(' → ')} → ${referencePath}`
772
+ };
773
+ }
774
+ // Look up the token
775
+ const token = registry.get(referencePath);
776
+ if (!token) {
777
+ return {
778
+ error: `Reference "{${referencePath}}" points to non-existent token`
779
+ };
780
+ }
781
+ // Mark this path as visited
782
+ visitedPaths.add(referencePath);
783
+ // Check if the token's value is itself a reference
784
+ if (isReference(token.$value)) {
785
+ const nextPath = extractReferencePath(token.$value);
786
+ return resolveReference(nextPath, registry, visitedPaths);
787
+ }
788
+ // Return the resolved value and type
789
+ return {
790
+ value: token.$value,
791
+ type: token.$type
792
+ };
793
+ }
794
+ /**
795
+ * Validates a token value based on its type
796
+ */
797
+ function validateTokenValue(token, path, errors, warnings, registry = null, parentType = null) {
798
+ let type = token.$type || parentType;
799
+ // A token is defined by either $value or a JSON Pointer $ref (§7.1.2).
800
+ if (!('$value' in token) && !('$ref' in token)) {
801
+ errors.push(`Token at ${path} is missing $value`);
802
+ return;
803
+ }
804
+ let value;
805
+ if ('$value' in token) {
806
+ value = token.$value;
807
+ // Curly-brace token reference at the value position.
808
+ if (isReference(value)) {
809
+ if (registry) {
810
+ const referencePath = extractReferencePath(value);
811
+ const resolved = resolveReference(referencePath, registry);
812
+ if (resolved.error) {
813
+ errors.push(`${resolved.error} at ${path}`);
814
+ return;
815
+ }
816
+ value = resolved.value;
817
+ if (!type) {
818
+ type = resolved.type;
819
+ }
820
+ }
821
+ else {
822
+ // References are allowed but not validated if no registry provided
823
+ return;
824
+ }
825
+ }
826
+ }
827
+ else {
828
+ // Token-level JSON Pointer $ref (alternative to $value): resolve the target.
829
+ const resolved = resolvePointerDeep(token.$ref, registry);
830
+ if (resolved.error) {
831
+ errors.push(`${resolved.error} at ${path}`);
832
+ return;
833
+ }
834
+ value = resolved.value;
835
+ }
836
+ // Check for unknown $type
837
+ if (type && !VALID_TOKEN_TYPES.includes(type)) {
838
+ warnings.push(`Unknown $type "${type}" at ${path}`);
839
+ }
840
+ if (!type) {
841
+ errors.push(`Token at ${path} has no determinable type (no $type property or group type)`);
842
+ return;
843
+ }
844
+ // Type-specific validation
845
+ switch (type) {
846
+ case 'color':
847
+ validateColorValue(value, path, errors, warnings);
848
+ break;
849
+ case 'dimension':
850
+ validateDimensionValue(value, path, errors);
851
+ break;
852
+ case 'fontFamily':
853
+ if (typeof value !== 'string' && !Array.isArray(value)) {
854
+ errors.push(`fontFamily at ${path} must be a string or array`);
855
+ }
856
+ break;
857
+ case 'fontWeight':
858
+ validateFontWeightValue(value, path, errors);
859
+ break;
860
+ case 'duration':
861
+ validateDurationValue(value, path, errors);
862
+ break;
863
+ case 'cubicBezier':
864
+ validateCubicBezierValue(value, path, errors);
865
+ break;
866
+ case 'number':
867
+ if (typeof value !== 'number') {
868
+ errors.push(`number at ${path} must be a number`);
869
+ }
870
+ break;
871
+ case 'strokeStyle':
872
+ validateStrokeStyleValue(value, path, errors, warnings);
873
+ break;
874
+ case 'border':
875
+ validateBorderValue(value, path, errors, warnings);
876
+ break;
877
+ case 'transition':
878
+ validateTransitionValue(value, path, errors, warnings);
879
+ break;
880
+ case 'shadow':
881
+ validateShadowValue(value, path, errors, warnings);
882
+ break;
883
+ case 'gradient':
884
+ validateGradientValue(value, path, errors, warnings);
885
+ break;
886
+ case 'typography':
887
+ validateTypographyValue(value, path, errors, warnings);
888
+ break;
889
+ }
890
+ }
891
+ /**
892
+ * Recursively validates tokens in an object
893
+ */
894
+ function validateToken(obj, path, errors, warnings, registry = null, parentType = null) {
895
+ // Validate group-level properties of the current (root) group.
896
+ if (path === '') {
897
+ validateDeprecated(obj, 'root', errors);
898
+ validateExtends(obj, 'root', errors);
899
+ }
900
+ for (const [key, value] of Object.entries(obj)) {
901
+ const currentPath = path ? `${path}.${key}` : key;
902
+ // $-prefixed keys: reserved properties are skipped; $root is a reserved
903
+ // root token (processed below); any other $-name is invalid (§5.1.1).
904
+ if (key.startsWith('$')) {
905
+ if (key !== '$root') {
906
+ if (!RESERVED_DOLLAR_KEYS.has(key)) {
907
+ errors.push(`Name "${key}" at ${currentPath} must not begin with "$" (reserved by the spec, §5.1.1)`);
908
+ }
909
+ continue;
910
+ }
911
+ }
912
+ // Check for invalid characters in token names
913
+ if (/[{}."]/.test(key)) {
914
+ errors.push(`Token name "${key}" at ${currentPath} contains invalid characters ({, }, ., or ")`);
915
+ }
916
+ // Check if this is a token (has $value or $ref) or a group
917
+ if (value && typeof value === 'object') {
918
+ validateDeprecated(value, currentPath, errors);
919
+ if ('$value' in value || '$ref' in value) {
920
+ // This is a token - validate it
921
+ validateTokenValue(value, currentPath, errors, warnings, registry, parentType);
922
+ }
923
+ else if ('$type' in value && Object.keys(value).filter(k => !k.startsWith('$') || k === '$root').length === 0) {
924
+ // Object has $type but no $value, $ref, $root, or child tokens/groups - invalid token
925
+ errors.push(`Token at ${currentPath} is missing $value`);
926
+ }
927
+ else {
928
+ // This is a group - validate group properties, then recurse
929
+ const groupType = value.$type || parentType;
930
+ validateExtends(value, currentPath, errors);
931
+ validateToken(value, currentPath, errors, warnings, registry, groupType);
932
+ }
933
+ }
934
+ }
935
+ }
936
+ /**
937
+ * Counts the total number of tokens in a token tree
938
+ */
939
+ function countTokens(obj, count = 0) {
940
+ for (const [key, value] of Object.entries(obj)) {
941
+ if (key.startsWith('$') && key !== '$root')
942
+ continue;
943
+ if (value && typeof value === 'object') {
944
+ if ('$value' in value || '$ref' in value) {
945
+ count++;
946
+ }
947
+ else {
948
+ count = countTokens(value, count);
949
+ }
950
+ }
951
+ }
952
+ return count;
953
+ }
954
+ /**
955
+ * Validates a design tokens JSON string against the W3C DTCG specification
956
+ */
957
+ export function validateTokens(jsonString) {
958
+ if (!jsonString || typeof jsonString !== 'string' || !jsonString.trim()) {
959
+ return { valid: false, errors: ['Input is empty'] };
960
+ }
961
+ try {
962
+ const tokens = JSON.parse(jsonString);
963
+ const errors = [];
964
+ const warnings = [];
965
+ // Validate root structure
966
+ if (typeof tokens !== 'object' || tokens === null || Array.isArray(tokens)) {
967
+ return { valid: false, errors: ['Root must be an object'] };
968
+ }
969
+ // Resolver documents (.resolver.json) follow a different schema.
970
+ if (isResolverDocument(tokens)) {
971
+ return validateResolverObject(tokens);
972
+ }
973
+ // Build token registry for reference resolution
974
+ _rootDoc = tokens;
975
+ const registry = buildTokenRegistry(tokens);
976
+ // Run validation
977
+ validateToken(tokens, '', errors, warnings, registry);
978
+ _rootDoc = null;
979
+ return {
980
+ valid: errors.length === 0,
981
+ errors,
982
+ warnings,
983
+ tokenCount: countTokens(tokens)
984
+ };
985
+ }
986
+ catch (error) {
987
+ _rootDoc = null;
988
+ return {
989
+ valid: false,
990
+ errors: [`Invalid JSON: ${error.message}`]
991
+ };
992
+ }
993
+ }
994
+ /**
995
+ * Validates a design tokens object (already parsed) against the W3C DTCG specification
996
+ */
997
+ export function validateTokensObject(tokens) {
998
+ if (!tokens) {
999
+ return { valid: false, errors: ['Input is empty'] };
1000
+ }
1001
+ const errors = [];
1002
+ const warnings = [];
1003
+ // Validate root structure
1004
+ if (typeof tokens !== 'object' || tokens === null || Array.isArray(tokens)) {
1005
+ return { valid: false, errors: ['Root must be an object'] };
1006
+ }
1007
+ // Resolver documents (.resolver.json) follow a different schema.
1008
+ if (isResolverDocument(tokens)) {
1009
+ return validateResolverObject(tokens);
1010
+ }
1011
+ // Build token registry for reference resolution
1012
+ _rootDoc = tokens;
1013
+ const registry = buildTokenRegistry(tokens);
1014
+ // Run validation
1015
+ validateToken(tokens, '', errors, warnings, registry);
1016
+ _rootDoc = null;
1017
+ return {
1018
+ valid: errors.length === 0,
1019
+ errors,
1020
+ warnings,
1021
+ tokenCount: countTokens(tokens)
1022
+ };
1023
+ }
1024
+ /**
1025
+ * Detects whether a parsed document is a Resolver document (Resolver Module)
1026
+ * rather than a token document.
1027
+ */
1028
+ function isResolverDocument(obj) {
1029
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
1030
+ return false;
1031
+ return (Array.isArray(obj.resolutionOrder) ||
1032
+ (typeof obj.version === 'string' && (typeof obj.sets === 'object' || typeof obj.modifiers === 'object')));
1033
+ }
1034
+ /**
1035
+ * Validates an array of token sources (used by sets and modifier contexts).
1036
+ * Each element must be a ReferenceObject ({ $ref }) or an inline tokens object.
1037
+ */
1038
+ function validateResolverSources(sources, label, errors) {
1039
+ if (!Array.isArray(sources)) {
1040
+ errors.push(`${label} must be an array of sources`);
1041
+ return;
1042
+ }
1043
+ sources.forEach((source, i) => {
1044
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
1045
+ errors.push(`${label}[${i}] must be a token source object or a { "$ref": … } reference`);
1046
+ }
1047
+ else if ('$ref' in source && typeof source.$ref !== 'string') {
1048
+ errors.push(`${label}[${i}].$ref must be a string`);
1049
+ }
1050
+ });
1051
+ }
1052
+ /**
1053
+ * Validates a Resolver document (.resolver.json) against the Resolver Module.
1054
+ * Returns the same result shape as validateTokens.
1055
+ */
1056
+ function validateResolverObject(doc) {
1057
+ const errors = [];
1058
+ const warnings = [];
1059
+ // Version (required, must be 2025.10)
1060
+ if (!('version' in doc)) {
1061
+ errors.push('Resolver document must declare a version (Resolver §4.1.2)');
1062
+ }
1063
+ else if (doc.version !== '2025.10') {
1064
+ errors.push('Resolver version must be "2025.10"');
1065
+ }
1066
+ if ('name' in doc && typeof doc.name !== 'string')
1067
+ errors.push('Resolver "name" must be a string');
1068
+ if ('description' in doc && typeof doc.description !== 'string')
1069
+ errors.push('Resolver "description" must be a string');
1070
+ // Sets
1071
+ if ('sets' in doc) {
1072
+ if (typeof doc.sets !== 'object' || doc.sets === null || Array.isArray(doc.sets)) {
1073
+ errors.push('Resolver "sets" must be a map of named sets');
1074
+ }
1075
+ else {
1076
+ for (const [name, set] of Object.entries(doc.sets)) {
1077
+ if (!set || typeof set !== 'object' || Array.isArray(set)) {
1078
+ errors.push(`Set "${name}" must be an object`);
1079
+ continue;
1080
+ }
1081
+ if (!('sources' in set)) {
1082
+ errors.push(`Set "${name}" must contain a sources array (Resolver §4.1.4)`);
1083
+ }
1084
+ else {
1085
+ validateResolverSources(set.sources, `Set "${name}" sources`, errors);
1086
+ }
1087
+ if ('description' in set && typeof set.description !== 'string') {
1088
+ errors.push(`Set "${name}" description must be a string`);
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ // Modifiers
1094
+ if ('modifiers' in doc) {
1095
+ if (typeof doc.modifiers !== 'object' || doc.modifiers === null || Array.isArray(doc.modifiers)) {
1096
+ errors.push('Resolver "modifiers" must be a map of named modifiers');
1097
+ }
1098
+ else {
1099
+ for (const [name, mod] of Object.entries(doc.modifiers)) {
1100
+ if (!mod || typeof mod !== 'object' || Array.isArray(mod)) {
1101
+ errors.push(`Modifier "${name}" must be an object`);
1102
+ continue;
1103
+ }
1104
+ if (!('contexts' in mod)) {
1105
+ errors.push(`Modifier "${name}" must declare a contexts map (Resolver §4.1.5.1)`);
1106
+ }
1107
+ else if (typeof mod.contexts !== 'object' || mod.contexts === null || Array.isArray(mod.contexts)) {
1108
+ errors.push(`Modifier "${name}" contexts must be a map`);
1109
+ }
1110
+ else {
1111
+ const contextKeys = Object.keys(mod.contexts);
1112
+ if (contextKeys.length === 0) {
1113
+ errors.push(`Modifier "${name}" must not have an empty contexts map`);
1114
+ }
1115
+ else if (contextKeys.length === 1) {
1116
+ warnings.push(`Modifier "${name}" should have two or more contexts (one is equivalent to a set)`);
1117
+ }
1118
+ for (const [ctxName, sources] of Object.entries(mod.contexts)) {
1119
+ validateResolverSources(sources, `Modifier "${name}" context "${ctxName}"`, errors);
1120
+ }
1121
+ if ('default' in mod && !contextKeys.includes(mod.default)) {
1122
+ errors.push(`Modifier "${name}" default "${mod.default}" must be one of its contexts`);
1123
+ }
1124
+ }
1125
+ if ('description' in mod && typeof mod.description !== 'string') {
1126
+ errors.push(`Modifier "${name}" description must be a string`);
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ // Resolution order (required)
1132
+ if (!('resolutionOrder' in doc)) {
1133
+ errors.push('Resolver document must declare resolutionOrder (Resolver §4.1.6)');
1134
+ }
1135
+ else if (!Array.isArray(doc.resolutionOrder)) {
1136
+ errors.push('resolutionOrder must be an array');
1137
+ }
1138
+ else {
1139
+ const inlineNames = new Set();
1140
+ doc.resolutionOrder.forEach((item, i) => {
1141
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
1142
+ errors.push(`resolutionOrder[${i}] must be a reference object or an inline set/modifier`);
1143
+ return;
1144
+ }
1145
+ if ('$ref' in item) {
1146
+ if (typeof item.$ref !== 'string') {
1147
+ errors.push(`resolutionOrder[${i}].$ref must be a string`);
1148
+ }
1149
+ else if (item.$ref.startsWith('#/resolutionOrder')) {
1150
+ errors.push(`resolutionOrder[${i}] must not reference the resolutionOrder array (Resolver §4.2.1)`);
1151
+ }
1152
+ return;
1153
+ }
1154
+ // Inline set/modifier: MUST have type and a unique name (Resolver §4.1.6.1)
1155
+ if (item.type !== 'set' && item.type !== 'modifier') {
1156
+ errors.push(`resolutionOrder[${i}] inline item must have type "set" or "modifier"`);
1157
+ }
1158
+ if (typeof item.name !== 'string') {
1159
+ errors.push(`resolutionOrder[${i}] inline item must have a name`);
1160
+ }
1161
+ else if (inlineNames.has(item.name)) {
1162
+ errors.push(`resolutionOrder inline name "${item.name}" is duplicated`);
1163
+ }
1164
+ else {
1165
+ inlineNames.add(item.name);
1166
+ }
1167
+ });
1168
+ }
1169
+ return {
1170
+ valid: errors.length === 0,
1171
+ errors,
1172
+ warnings,
1173
+ documentType: 'resolver',
1174
+ tokenCount: 0,
1175
+ resolutionCount: Array.isArray(doc.resolutionOrder) ? doc.resolutionOrder.length : 0
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Analyzes validation errors and provides detailed insights with suggestions
1180
+ */
1181
+ export function analyzeErrors(validationResult) {
1182
+ if (!validationResult.errors || validationResult.errors.length === 0) {
1183
+ return {
1184
+ ...validationResult,
1185
+ analysis: {
1186
+ summary: 'No errors found',
1187
+ categories: {},
1188
+ suggestions: []
1189
+ }
1190
+ };
1191
+ }
1192
+ const analysis = {
1193
+ categories: {
1194
+ structure: [],
1195
+ type: [],
1196
+ value: [],
1197
+ naming: [],
1198
+ reference: []
1199
+ },
1200
+ suggestions: []
1201
+ };
1202
+ // Analyze each error
1203
+ validationResult.errors.forEach((error, index) => {
1204
+ const analyzed = analyzeError(error, index + 1);
1205
+ analysis.categories[analyzed.category].push(analyzed);
1206
+ if (analyzed.suggestion) {
1207
+ analysis.suggestions.push(analyzed.suggestion);
1208
+ }
1209
+ });
1210
+ // Create summary
1211
+ const errorCounts = Object.entries(analysis.categories)
1212
+ .filter(([_, errors]) => errors.length > 0)
1213
+ .map(([category, errors]) => `${errors.length} ${category}`)
1214
+ .join(', ');
1215
+ analysis.summary = `Found ${validationResult.errors.length} error(s): ${errorCounts}`;
1216
+ return {
1217
+ ...validationResult,
1218
+ analysis
1219
+ };
1220
+ }
1221
+ /**
1222
+ * Analyzes a single error and provides category, context, and suggestions
1223
+ */
1224
+ function analyzeError(errorMessage, errorNumber) {
1225
+ const error = {
1226
+ number: errorNumber,
1227
+ message: errorMessage,
1228
+ category: 'value',
1229
+ path: extractPath(errorMessage),
1230
+ suggestion: null,
1231
+ details: null
1232
+ };
1233
+ // Categorize and add suggestions based on error patterns
1234
+ if (errorMessage.includes('Invalid JSON')) {
1235
+ error.category = 'structure';
1236
+ error.suggestion = 'Check for missing commas, brackets, or quotes. Use a JSON validator to find syntax errors.';
1237
+ error.details = 'The input is not valid JSON. Common issues include trailing commas, unquoted keys, or mismatched brackets.';
1238
+ }
1239
+ else if (errorMessage.includes('Root must be an object')) {
1240
+ error.category = 'structure';
1241
+ error.suggestion = 'Wrap your tokens in a JSON object with curly braces: { "color": { ... } }';
1242
+ error.details = 'Design tokens must be defined within a root object, not as an array or primitive value.';
1243
+ }
1244
+ else if (errorMessage.includes('Input is empty')) {
1245
+ error.category = 'structure';
1246
+ error.suggestion = 'Paste your design tokens JSON into the editor.';
1247
+ error.details = 'No input was provided for validation.';
1248
+ }
1249
+ else if (errorMessage.includes('missing $value')) {
1250
+ error.category = 'structure';
1251
+ error.suggestion = `Add a "$value" property to the token at ${error.path}. Example: "$value": "#ff0000"`;
1252
+ error.details = 'All tokens must have a $value property that contains the actual token value.';
1253
+ }
1254
+ else if (errorMessage.includes('Unknown $type')) {
1255
+ error.category = 'type';
1256
+ const match = errorMessage.match(/"([^"]+)"/);
1257
+ if (match) {
1258
+ error.suggestion = `Change "$type": "${match[1]}" to one of: ${VALID_TOKEN_TYPES.join(', ')}`;
1259
+ }
1260
+ error.details = 'The $type must be one of the 13 supported token types defined in the DTCG Format Module.';
1261
+ }
1262
+ else if (errorMessage.includes('contains invalid characters')) {
1263
+ error.category = 'naming';
1264
+ error.suggestion = 'Token names cannot contain dots (.), curly braces ({ }), or quotes ("). Use hyphens or underscores instead.';
1265
+ error.details = 'Token names must follow DTCG naming conventions: alphanumeric, hyphens, and underscores only.';
1266
+ }
1267
+ else if (errorMessage.includes('colorSpace')) {
1268
+ error.category = 'value';
1269
+ if (errorMessage.includes('must have colorSpace')) {
1270
+ error.suggestion = `Add "colorSpace" property. Example: "colorSpace": "srgb"`;
1271
+ }
1272
+ else if (errorMessage.includes('unsupported colorSpace')) {
1273
+ const supportedSpaces = Object.keys(COLOR_SPACES).join(', ');
1274
+ error.suggestion = `Use one of the 14 supported color spaces: ${supportedSpaces}`;
1275
+ }
1276
+ error.details = 'Color tokens using object format must specify a valid colorSpace from the Color Module 2025.10.';
1277
+ }
1278
+ else if (errorMessage.includes('components')) {
1279
+ error.category = 'value';
1280
+ if (errorMessage.includes('must have components array')) {
1281
+ error.suggestion = 'Add "components" array with color values. Example: "components": [1, 0, 0] for red in sRGB.';
1282
+ }
1283
+ else if (errorMessage.includes('exactly 3 values')) {
1284
+ error.suggestion = 'Color components must be an array of exactly 3 numeric values or "none".';
1285
+ }
1286
+ else if (errorMessage.includes('must be a number or "none"')) {
1287
+ error.suggestion = 'Each component must be either a number or the string "none".';
1288
+ }
1289
+ else if (errorMessage.includes('must be between')) {
1290
+ const match = errorMessage.match(/between ([\d.]+) and ([\d.]+)/);
1291
+ if (match) {
1292
+ error.suggestion = `Component value must be in range [${match[1]}, ${match[2]}]. Check the color space requirements.`;
1293
+ }
1294
+ }
1295
+ else if (errorMessage.includes('must be >= 0 and < 360')) {
1296
+ error.suggestion = 'Hue values must be in the range [0, 360) - note that 360 is NOT valid, use 0 instead for a full rotation.';
1297
+ }
1298
+ error.details = 'Color components must conform to the range requirements of their color space.';
1299
+ }
1300
+ else if (errorMessage.includes('alpha')) {
1301
+ error.category = 'value';
1302
+ error.suggestion = 'Alpha must be a number between 0 and 1, where 0 is fully transparent and 1 is fully opaque.';
1303
+ error.details = 'The alpha channel controls opacity and must be a numeric value in the range [0, 1].';
1304
+ }
1305
+ else if (errorMessage.includes('fontWeight')) {
1306
+ error.category = 'value';
1307
+ if (errorMessage.includes('between 1-1000')) {
1308
+ error.suggestion = `Use a numeric weight between 1-1000, or use an alias like: ${Object.keys(FONT_WEIGHT_ALIASES).slice(0, 5).join(', ')}, etc.`;
1309
+ }
1310
+ else if (errorMessage.includes('valid weight alias')) {
1311
+ error.suggestion = `Use one of these aliases: ${Object.keys(FONT_WEIGHT_ALIASES).join(', ')}`;
1312
+ }
1313
+ error.details = 'Font weight must be a number 1-1000 or a recognized weight alias.';
1314
+ }
1315
+ else if (errorMessage.includes('dimension') && errorMessage.includes('unit')) {
1316
+ error.category = 'value';
1317
+ error.suggestion = 'Dimension units must be "px" or "rem" per the DTCG Format Module 2025.10.';
1318
+ error.details = 'The specification only allows "px" and "rem" units for dimensions.';
1319
+ }
1320
+ else if (errorMessage.includes('duration') && errorMessage.includes('unit')) {
1321
+ error.category = 'value';
1322
+ error.suggestion = 'Duration units must be "ms" (milliseconds) or "s" (seconds).';
1323
+ error.details = 'Duration values must use milliseconds or seconds as the unit.';
1324
+ }
1325
+ else if (errorMessage.includes('cubicBezier')) {
1326
+ error.category = 'value';
1327
+ if (errorMessage.includes('exactly 4 numbers')) {
1328
+ error.suggestion = 'cubicBezier must be an array of 4 numbers: [P1x, P1y, P2x, P2y]. Example: [0.42, 0, 0.58, 1]';
1329
+ }
1330
+ else if (errorMessage.includes('must be in range [0, 1]')) {
1331
+ error.suggestion = 'X coordinates (P1x and P2x) must be between 0 and 1. Y coordinates can be any value.';
1332
+ }
1333
+ error.details = 'Cubic bezier values define easing curves with control points.';
1334
+ }
1335
+ else if (errorMessage.includes('Shadow') && errorMessage.includes('missing required field')) {
1336
+ error.category = 'structure';
1337
+ const match = errorMessage.match(/field: (\w+)/);
1338
+ if (match) {
1339
+ error.suggestion = `Add the "${match[1]}" property to your shadow object. Shadows require: offsetX, offsetY, blur, spread, and color.`;
1340
+ }
1341
+ error.details = 'Shadow tokens must have all required fields: offsetX, offsetY, blur, spread, and color. Inset is optional.';
1342
+ }
1343
+ else if (errorMessage.includes('Typography') && errorMessage.includes('missing required field')) {
1344
+ error.category = 'structure';
1345
+ const match = errorMessage.match(/field: (\w+)/);
1346
+ if (match) {
1347
+ error.suggestion = `Add the "${match[1]}" property. Typography requires: fontFamily, fontSize, fontWeight, lineHeight, and letterSpacing.`;
1348
+ }
1349
+ error.details = 'Typography tokens must have all 5 required fields per the Format Module.';
1350
+ }
1351
+ else if (errorMessage.includes('strokeStyle')) {
1352
+ error.category = 'value';
1353
+ error.suggestion = `Use one of: ${STROKE_STYLE_VALUES.join(', ')}, or provide an object with dashArray and optional lineCap.`;
1354
+ error.details = 'Stroke style can be a predefined string or a custom object with dash patterns.';
1355
+ }
1356
+ else if (errorMessage.includes('Gradient')) {
1357
+ error.category = 'value';
1358
+ if (errorMessage.includes('must be an array')) {
1359
+ error.suggestion = 'Gradients must be an array of stops. Example: [{"color": "#000", "position": 0}, {"color": "#fff", "position": 1}]';
1360
+ }
1361
+ else if (errorMessage.includes('must have color property')) {
1362
+ error.suggestion = 'Each gradient stop must have a "color" property.';
1363
+ }
1364
+ else if (errorMessage.includes('must have position property')) {
1365
+ error.suggestion = 'Each gradient stop must have a "position" property (typically 0 to 1).';
1366
+ }
1367
+ error.details = 'Gradients are arrays of color stops, each with a color and position.';
1368
+ }
1369
+ else if (errorMessage.includes('Border') && errorMessage.includes('must have')) {
1370
+ error.category = 'structure';
1371
+ const match = errorMessage.match(/must have (\w+) property/);
1372
+ if (match) {
1373
+ error.suggestion = `Add the "${match[1]}" property. Border requires: color, width, and style.`;
1374
+ }
1375
+ error.details = 'Border tokens are composite types requiring color, width, and style properties.';
1376
+ }
1377
+ else if (errorMessage.includes('Transition') && errorMessage.includes('must have')) {
1378
+ error.category = 'structure';
1379
+ const match = errorMessage.match(/must have (\w+) property/);
1380
+ if (match) {
1381
+ error.suggestion = `Add the "${match[1]}" property. Transition requires: duration, delay, and timingFunction.`;
1382
+ }
1383
+ error.details = 'Transition tokens define animation timing with duration, delay, and easing.';
1384
+ }
1385
+ return error;
1386
+ }
1387
+ /**
1388
+ * Extracts the token path from an error message
1389
+ */
1390
+ function extractPath(errorMessage) {
1391
+ // Try to find "at <path>" pattern
1392
+ const atMatch = errorMessage.match(/at ([\w.[\]]+)/);
1393
+ if (atMatch) {
1394
+ return atMatch[1];
1395
+ }
1396
+ // Try to find path in quotes
1397
+ const quoteMatch = errorMessage.match(/"([\w.]+)"/);
1398
+ if (quoteMatch && !quoteMatch[1].includes(' ')) {
1399
+ return quoteMatch[1];
1400
+ }
1401
+ return 'root';
1402
+ }
1403
+ //# sourceMappingURL=validate.js.map