@terrazzo/parser 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/build/index.d.ts +104 -0
- package/build/index.js +182 -0
- package/config.d.ts +64 -0
- package/config.js +196 -0
- package/index.d.ts +16 -0
- package/index.js +37 -0
- package/lint/index.d.ts +41 -0
- package/lint/index.js +59 -0
- package/lint/plugin-core/index.d.ts +3 -0
- package/lint/plugin-core/index.js +12 -0
- package/lint/plugin-core/rules/duplicate-values.d.ts +10 -0
- package/lint/plugin-core/rules/duplicate-values.js +69 -0
- package/logger.d.ts +66 -0
- package/logger.js +121 -0
- package/package.json +52 -0
- package/parse/index.d.ts +32 -0
- package/parse/index.js +372 -0
- package/parse/json.d.ts +30 -0
- package/parse/json.js +94 -0
- package/parse/normalize.d.ts +3 -0
- package/parse/normalize.js +114 -0
- package/parse/validate.d.ts +42 -0
- package/parse/validate.js +620 -0
- package/parse/yaml.d.ts +11 -0
- package/parse/yaml.js +45 -0
- package/types.d.ts +519 -0
- package/types.js +1 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { print } from '@humanwhocodes/momoa';
|
|
2
|
+
import { isAlias } from '@terrazzo/token-tools';
|
|
3
|
+
import { getObjMembers } from './json.js';
|
|
4
|
+
|
|
5
|
+
const listFormat = new Intl.ListFormat('en-us', { type: 'disjunction' });
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import("@humanwhocodes/momoa").AnyNode} AnyNode
|
|
9
|
+
* @typedef {import("@humanwhocodes/momoa").ObjectNode} ObjectNode
|
|
10
|
+
* @typedef {import("@humanwhocodes/momoa").ValueNode} ValueNode
|
|
11
|
+
* @typedef {import("@babel/code-frame").SourceLocation} SourceLocation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const FONT_WEIGHT_VALUES = new Set([
|
|
15
|
+
'thin',
|
|
16
|
+
'hairline',
|
|
17
|
+
'extra-light',
|
|
18
|
+
'ultra-light',
|
|
19
|
+
'light',
|
|
20
|
+
'normal',
|
|
21
|
+
'regular',
|
|
22
|
+
'book',
|
|
23
|
+
'medium',
|
|
24
|
+
'semi-bold',
|
|
25
|
+
'demi-bold',
|
|
26
|
+
'bold',
|
|
27
|
+
'extra-bold',
|
|
28
|
+
'ultra-bold',
|
|
29
|
+
'black',
|
|
30
|
+
'heavy',
|
|
31
|
+
'extra-black',
|
|
32
|
+
'ultra-black',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const STROKE_STYLE_VALUES = new Set([
|
|
36
|
+
'solid',
|
|
37
|
+
'dashed',
|
|
38
|
+
'dotted',
|
|
39
|
+
'double',
|
|
40
|
+
'groove',
|
|
41
|
+
'ridge',
|
|
42
|
+
'outset',
|
|
43
|
+
'inset',
|
|
44
|
+
]);
|
|
45
|
+
export const STROKE_STYLE_LINE_CAP_VALUES = new Set(['round', 'butt', 'square']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the location of a JSON node
|
|
49
|
+
* @param {AnyNode} node
|
|
50
|
+
* @return {SourceLocation}
|
|
51
|
+
*/
|
|
52
|
+
function getLoc(node) {
|
|
53
|
+
return { line: node.loc?.start.line ?? 1, column: node.loc?.start.column };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Distinct from isAlias() in that this accepts malformed aliases
|
|
58
|
+
* @param {AnyNode} node
|
|
59
|
+
* @return {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isMaybeAlias(node) {
|
|
62
|
+
if (node?.type === 'String') {
|
|
63
|
+
return node.value.startsWith('{');
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Assert object members match given types
|
|
70
|
+
* @param {ObjectNode} $value
|
|
71
|
+
* @param {Record<string, { validator: typeof validateAlias; required?: boolean }>} properties
|
|
72
|
+
* @param {AnyNode} node
|
|
73
|
+
* @param {ValidateOptions} options
|
|
74
|
+
* @return {void}
|
|
75
|
+
*/
|
|
76
|
+
function validateMembersAs($value, properties, node, { ast, logger }) {
|
|
77
|
+
const members = getObjMembers($value);
|
|
78
|
+
for (const property in properties) {
|
|
79
|
+
const { validator, required } = properties[property];
|
|
80
|
+
if (!members[property]) {
|
|
81
|
+
if (required) {
|
|
82
|
+
logger.error({ message: `Missing required property "${property}"`, node, ast, loc: getLoc($value) });
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const value = members[property];
|
|
87
|
+
if (isMaybeAlias(value)) {
|
|
88
|
+
validateAlias(value, node, { ast, logger });
|
|
89
|
+
} else {
|
|
90
|
+
validator(value, node, { ast, logger });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verify an Alias token is valid
|
|
97
|
+
* @param {ValueNode} $value
|
|
98
|
+
* @param {AnyNode} node
|
|
99
|
+
* @param {ValidateOptions} options
|
|
100
|
+
* @return {void}
|
|
101
|
+
*/
|
|
102
|
+
export function validateAlias($value, node, { ast, logger }) {
|
|
103
|
+
if ($value.type !== 'String' || !isAlias($value.value)) {
|
|
104
|
+
logger.error({ message: `Invalid alias: ${print($value)}`, node, ast, loc: getLoc($value) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Verify a Border token is valid
|
|
110
|
+
* @param {ValueNode} $value
|
|
111
|
+
* @param {AnyNode} node
|
|
112
|
+
* @param {ValidateOptions} options
|
|
113
|
+
* @return {void}
|
|
114
|
+
*/
|
|
115
|
+
export function validateBorder($value, node, { ast, logger }) {
|
|
116
|
+
if ($value.type !== 'Object') {
|
|
117
|
+
logger.error({ message: `Expected object, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
validateMembersAs(
|
|
121
|
+
$value,
|
|
122
|
+
{
|
|
123
|
+
color: { validator: validateColor, required: true },
|
|
124
|
+
style: { validator: validateStrokeStyle, required: true },
|
|
125
|
+
width: { validator: validateDimension, required: true },
|
|
126
|
+
},
|
|
127
|
+
node,
|
|
128
|
+
{ ast, logger },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify a Color token is valid
|
|
134
|
+
* @param {ValueNode} $value
|
|
135
|
+
* @param {AnyNode} node
|
|
136
|
+
* @param {ValidateOptions} options
|
|
137
|
+
* @return {void}
|
|
138
|
+
*/
|
|
139
|
+
export function validateColor($value, node, { ast, logger }) {
|
|
140
|
+
if ($value.type !== 'String') {
|
|
141
|
+
logger.error({ message: `Expected string, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
142
|
+
} else if ($value.value === '') {
|
|
143
|
+
logger.error({ message: 'Expected color, received empty string', node, ast, loc: getLoc($value) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Verify a Cubic Bézier token is valid
|
|
149
|
+
* @param {ValueNode} $value
|
|
150
|
+
* @param {AnyNode} node
|
|
151
|
+
* @param {ValidateOptions} options
|
|
152
|
+
* @return {void}
|
|
153
|
+
*/
|
|
154
|
+
export function validateCubicBézier($value, node, { ast, logger }) {
|
|
155
|
+
if ($value.type !== 'Array') {
|
|
156
|
+
logger.error({
|
|
157
|
+
message: `Expected array of strings, received ${print($value)}`,
|
|
158
|
+
node,
|
|
159
|
+
ast,
|
|
160
|
+
loc: getLoc($value),
|
|
161
|
+
});
|
|
162
|
+
} else if (
|
|
163
|
+
!$value.elements.every((e) => e.value.type === 'Number' || (e.value.type === 'String' && isAlias(e.value.value)))
|
|
164
|
+
) {
|
|
165
|
+
logger.error({
|
|
166
|
+
message: 'Expected an array of 4 numbers, received some non-numbers',
|
|
167
|
+
node,
|
|
168
|
+
ast,
|
|
169
|
+
loc: getLoc($value),
|
|
170
|
+
});
|
|
171
|
+
} else if ($value.elements.length !== 4) {
|
|
172
|
+
logger.error({
|
|
173
|
+
message: `Expected an array of 4 numbers, received ${$value.elements.length}`,
|
|
174
|
+
node,
|
|
175
|
+
ast,
|
|
176
|
+
loc: getLoc($value),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Verify a Dimension token is valid
|
|
183
|
+
* @param {ValueNode} $value
|
|
184
|
+
* @param {AnyNode} node
|
|
185
|
+
* @param {ValidateOptions} options
|
|
186
|
+
* @return {void}
|
|
187
|
+
*/
|
|
188
|
+
export function validateDimension($value, node, { ast, logger }) {
|
|
189
|
+
if ($value.type === 'Number' && $value.value === 0) {
|
|
190
|
+
return; // `0` is a valid number
|
|
191
|
+
}
|
|
192
|
+
if ($value.type !== 'String') {
|
|
193
|
+
logger.error({ message: `Expected string, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
194
|
+
} else if ($value.value === '') {
|
|
195
|
+
logger.error({ message: 'Expected dimension, received empty string', node, ast, loc: getLoc($value) });
|
|
196
|
+
} else if (String(Number($value.value)) === $value.value) {
|
|
197
|
+
logger.error({ message: 'Missing units', node, ast, loc: getLoc($value) });
|
|
198
|
+
} else if (!/^[0-9]+/.test($value.value)) {
|
|
199
|
+
logger.error({
|
|
200
|
+
message: `Expected dimension with units, received ${print($value)}`,
|
|
201
|
+
node,
|
|
202
|
+
ast,
|
|
203
|
+
loc: getLoc($value),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Verify a Duration token is valid
|
|
210
|
+
* @param {ValueNode} $value
|
|
211
|
+
* @param {AnyNode} node
|
|
212
|
+
* @param {ValidateOptions} options
|
|
213
|
+
* @return {void}
|
|
214
|
+
*/
|
|
215
|
+
export function validateDuration($value, node, { ast, logger }) {
|
|
216
|
+
if ($value.type === 'Number' && $value.value === 0) {
|
|
217
|
+
return; // `0` is a valid number
|
|
218
|
+
}
|
|
219
|
+
if ($value.type !== 'String') {
|
|
220
|
+
logger.error({ message: `Expected string, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
221
|
+
} else if ($value.value === '') {
|
|
222
|
+
logger.error({ message: 'Expected duration, received empty string', node, ast, loc: getLoc($value) });
|
|
223
|
+
} else if (!/m?s$/.test($value.value)) {
|
|
224
|
+
logger.error({ message: 'Missing unit "ms" or "s"', node, ast, loc: getLoc($value) });
|
|
225
|
+
} else if (!/^[0-9]+/.test($value.value)) {
|
|
226
|
+
logger.error({
|
|
227
|
+
message: `Expected duration in \`ms\` or \`s\`, received ${print($value)}`,
|
|
228
|
+
node,
|
|
229
|
+
ast,
|
|
230
|
+
loc: getLoc($value),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Verify a Font Family token is valid
|
|
237
|
+
* @param {ValueNode} $value
|
|
238
|
+
* @param {AnyNode} node
|
|
239
|
+
* @param {ValidateOptions} options
|
|
240
|
+
* @return {void}
|
|
241
|
+
*/
|
|
242
|
+
export function validateFontFamily($value, node, { ast, logger }) {
|
|
243
|
+
if ($value.type !== 'String' && $value.type !== 'Array') {
|
|
244
|
+
logger.error({
|
|
245
|
+
message: `Expected string or array of strings, received ${$value.type}`,
|
|
246
|
+
node,
|
|
247
|
+
ast,
|
|
248
|
+
loc: getLoc($value),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if ($value.type === 'String' && $value.value === '') {
|
|
252
|
+
logger.error({ message: 'Expected font family name, received empty string', node, ast, loc: getLoc($value) });
|
|
253
|
+
}
|
|
254
|
+
if ($value.type === 'Array' && !$value.elements.every((e) => e.value.type === 'String' && e.value.value !== '')) {
|
|
255
|
+
logger.error({
|
|
256
|
+
message: 'Expected an array of strings, received some non-strings or empty strings',
|
|
257
|
+
node,
|
|
258
|
+
ast,
|
|
259
|
+
loc: getLoc($value),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Verify a Font Weight token is valid
|
|
266
|
+
* @param {ValueNode} $value
|
|
267
|
+
* @param {AnyNode} node
|
|
268
|
+
* @param {ValidateOptions} options
|
|
269
|
+
* @return {void}
|
|
270
|
+
*/
|
|
271
|
+
export function validateFontWeight($value, node, { ast, logger }) {
|
|
272
|
+
if ($value.type !== 'String' && $value.type !== 'Number') {
|
|
273
|
+
logger.error({
|
|
274
|
+
message: `Expected a font weight name or number 0–1000, received ${$value.type}`,
|
|
275
|
+
node,
|
|
276
|
+
ast,
|
|
277
|
+
loc: getLoc($value),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if ($value.type === 'String' && !FONT_WEIGHT_VALUES.has($value.value)) {
|
|
281
|
+
logger.error({
|
|
282
|
+
message: `Unknown font weight ${print($value)}. Expected one of: ${listFormat.format([...FONT_WEIGHT_VALUES])}.`,
|
|
283
|
+
node,
|
|
284
|
+
ast,
|
|
285
|
+
loc: getLoc($value),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if ($value.type === 'Number' && ($value.value < 0 || $value.value > 1000)) {
|
|
289
|
+
logger.error({ message: `Expected number 0–1000, received ${print($value)}`, node, ast, loc: getLoc($value) });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Verify a Gradient token is valid
|
|
295
|
+
* @param {ValueNode} $value
|
|
296
|
+
* @param {AnyNode} node
|
|
297
|
+
* @param {ValidateOptions} options
|
|
298
|
+
* @return {void}
|
|
299
|
+
*/
|
|
300
|
+
export function validateGradient($value, node, { ast, logger }) {
|
|
301
|
+
if ($value.type !== 'Array') {
|
|
302
|
+
logger.error({
|
|
303
|
+
message: `Expected array of gradient stops, received ${$value.type}`,
|
|
304
|
+
node,
|
|
305
|
+
ast,
|
|
306
|
+
loc: getLoc($value),
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (let i = 0; i < $value.elements.length; i++) {
|
|
311
|
+
const element = $value.elements[i];
|
|
312
|
+
if (element.value.type !== 'Object') {
|
|
313
|
+
logger.error({
|
|
314
|
+
message: `Stop #${i + 1}: Expected gradient stop, received ${element.value.type}`,
|
|
315
|
+
node,
|
|
316
|
+
ast,
|
|
317
|
+
loc: getLoc(element),
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
validateMembersAs(
|
|
322
|
+
element.value,
|
|
323
|
+
{
|
|
324
|
+
color: { validator: validateColor, required: true },
|
|
325
|
+
position: { validator: validateNumber, required: true },
|
|
326
|
+
},
|
|
327
|
+
node,
|
|
328
|
+
{ ast, logger },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Verify a Number token is valid
|
|
335
|
+
* @param {ValueNode} $value
|
|
336
|
+
* @param {AnyNode} node
|
|
337
|
+
* @param {ValidateOptions} options
|
|
338
|
+
* @return {void}
|
|
339
|
+
*/
|
|
340
|
+
export function validateNumber($value, node, { ast, logger }) {
|
|
341
|
+
if ($value.type !== 'Number') {
|
|
342
|
+
logger.error({ message: `Expected number, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Verify a Shadow token’s value is valid
|
|
348
|
+
* @param {ValueNode} $value
|
|
349
|
+
* @param {AnyNode} node
|
|
350
|
+
* @param {ValidateOptions} options
|
|
351
|
+
* @return {void}
|
|
352
|
+
*/
|
|
353
|
+
export function validateShadowLayer($value, node, { ast, logger }) {
|
|
354
|
+
if ($value.type !== 'Object') {
|
|
355
|
+
logger.error({ message: `Expected Object, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
validateMembersAs(
|
|
359
|
+
$value,
|
|
360
|
+
{
|
|
361
|
+
color: { validator: validateColor, required: true },
|
|
362
|
+
offsetX: { validator: validateDimension, required: true },
|
|
363
|
+
offsetY: { validator: validateDimension, required: true },
|
|
364
|
+
blur: { validator: validateDimension },
|
|
365
|
+
spread: { validator: validateDimension },
|
|
366
|
+
},
|
|
367
|
+
node,
|
|
368
|
+
{ ast, logger },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Verify a Stroke Style token is valid.
|
|
374
|
+
* @param {ValueNode} $value
|
|
375
|
+
* @param {AnyNode} node
|
|
376
|
+
* @param {ValidateOptions} options
|
|
377
|
+
* @return {void}
|
|
378
|
+
*/
|
|
379
|
+
export function validateStrokeStyle($value, node, { ast, logger }) {
|
|
380
|
+
// note: strokeStyle’s values are NOT aliasable (unless by string, but that breaks validations)
|
|
381
|
+
if ($value.type === 'String') {
|
|
382
|
+
if (!STROKE_STYLE_VALUES.has($value.value)) {
|
|
383
|
+
logger.error({
|
|
384
|
+
message: `Unknown stroke style ${print($value)}. Expected one of: ${listFormat.format([
|
|
385
|
+
...STROKE_STYLE_VALUES,
|
|
386
|
+
])}.`,
|
|
387
|
+
node,
|
|
388
|
+
ast,
|
|
389
|
+
loc: getLoc($value),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
} else if ($value.type === 'Object') {
|
|
393
|
+
const strokeMembers = getObjMembers($value);
|
|
394
|
+
for (const property of ['dashArray', 'lineCap']) {
|
|
395
|
+
if (!strokeMembers[property]) {
|
|
396
|
+
logger.error({
|
|
397
|
+
message: `Missing required property "${property}"`,
|
|
398
|
+
node,
|
|
399
|
+
ast,
|
|
400
|
+
loc: getLoc($value),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const { lineCap, dashArray } = strokeMembers;
|
|
405
|
+
if (lineCap?.type !== 'String' || !STROKE_STYLE_LINE_CAP_VALUES.has(lineCap.value)) {
|
|
406
|
+
logger.error({
|
|
407
|
+
message: `Unknown lineCap value ${print(lineCap)}. Expected one of: ${listFormat.format([
|
|
408
|
+
...STROKE_STYLE_LINE_CAP_VALUES,
|
|
409
|
+
])}.`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (dashArray?.type === 'Array') {
|
|
413
|
+
for (const element of dashArray.elements) {
|
|
414
|
+
if (element.value.type === 'String' && element.value.value !== '') {
|
|
415
|
+
if (isMaybeAlias(element.value)) {
|
|
416
|
+
validateAlias(element.value, node, { logger, ast });
|
|
417
|
+
} else {
|
|
418
|
+
validateDimension(element.value, node, { logger, ast });
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
logger.error({
|
|
422
|
+
message: 'Expected array of strings, recieved some non-strings or empty strings.',
|
|
423
|
+
node,
|
|
424
|
+
ast,
|
|
425
|
+
loc: getLoc(element),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
logger.error({
|
|
431
|
+
message: `Expected array of strings, received ${dashArray.type}`,
|
|
432
|
+
node,
|
|
433
|
+
ast,
|
|
434
|
+
loc: getLoc($value),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
logger.error({ message: `Expected string or object, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Verify a Transition token is valid
|
|
444
|
+
* @param {ValueNode} $value
|
|
445
|
+
* @param {AnyNode} node
|
|
446
|
+
* @param {ValidateOptions} options
|
|
447
|
+
* @return {void}
|
|
448
|
+
*/
|
|
449
|
+
export function validateTransition($value, node, { ast, logger }) {
|
|
450
|
+
if ($value.type !== 'Object') {
|
|
451
|
+
logger.error({ message: `Expected object, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
validateMembersAs(
|
|
455
|
+
$value,
|
|
456
|
+
{
|
|
457
|
+
duration: { validator: validateDuration, required: true },
|
|
458
|
+
delay: { validator: validateDuration, required: false }, // note: spec says delay is required, but Terrazzo makes delay optional
|
|
459
|
+
timingFunction: { validator: validateCubicBézier, required: true },
|
|
460
|
+
},
|
|
461
|
+
node,
|
|
462
|
+
{ ast, logger },
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Validate a MemberNode (the entire token object, plus its key in the parent object) to see if it’s a valid DTCG token or not.
|
|
468
|
+
* Keeping the parent key really helps in debug messages.
|
|
469
|
+
* @param {AnyNode} node
|
|
470
|
+
* @param {ValidateOptions} options
|
|
471
|
+
* @return {void}
|
|
472
|
+
*/
|
|
473
|
+
export default function validate(node, { ast, logger }) {
|
|
474
|
+
if (node.type !== 'Member' && node.type !== 'Object') {
|
|
475
|
+
logger.error({
|
|
476
|
+
message: `Expected Object, received ${JSON.stringify(node.type)}`,
|
|
477
|
+
node,
|
|
478
|
+
ast,
|
|
479
|
+
loc: { line: 1 },
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const rootMembers = node.value.type === 'Object' ? getObjMembers(node.value) : {};
|
|
485
|
+
const $value = rootMembers.$value;
|
|
486
|
+
const $type = rootMembers.$type;
|
|
487
|
+
|
|
488
|
+
if (!$value) {
|
|
489
|
+
logger.error({ message: 'Token missing $value', node, ast, loc: getLoc(node) });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// If top-level value is a valid alias, this is valid (no need for $type)
|
|
493
|
+
// ⚠️ Important: ALL Object and Array nodes below will need to check for aliases within!
|
|
494
|
+
if (isMaybeAlias($value)) {
|
|
495
|
+
validateAlias($value, node, { logger, ast });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!$type) {
|
|
500
|
+
logger.error({ message: 'Token missing $type', node, ast, loc: getLoc(node) });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
switch ($type.value) {
|
|
505
|
+
case 'color': {
|
|
506
|
+
validateColor($value, node, { logger, ast });
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'cubicBezier': {
|
|
510
|
+
validateCubicBézier($value, node, { logger, ast });
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case 'dimension': {
|
|
514
|
+
validateDimension($value, node, { logger, ast });
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 'duration': {
|
|
518
|
+
validateDuration($value, node, { logger, ast });
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case 'fontFamily': {
|
|
522
|
+
validateFontFamily($value, node, { logger, ast });
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case 'fontWeight': {
|
|
526
|
+
validateFontWeight($value, node, { logger, ast });
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'number': {
|
|
530
|
+
validateNumber($value, node, { logger, ast });
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case 'shadow': {
|
|
534
|
+
if ($value.type === 'Object') {
|
|
535
|
+
validateShadowLayer($value, node, { logger, ast });
|
|
536
|
+
} else if ($value.type === 'Array') {
|
|
537
|
+
for (const element of $value.elements) {
|
|
538
|
+
validateShadowLayer(element.value, $value, { logger, ast });
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
logger.error({
|
|
542
|
+
message: `Expected shadow object or array of shadow objects, received ${$value.type}`,
|
|
543
|
+
node,
|
|
544
|
+
ast,
|
|
545
|
+
loc: getLoc($value),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// extensions
|
|
552
|
+
case 'boolean': {
|
|
553
|
+
if ($value.type !== 'Boolean') {
|
|
554
|
+
logger.error({ message: `Expected boolean, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case 'link': {
|
|
559
|
+
if ($value.type !== 'String') {
|
|
560
|
+
logger.error({ message: `Expected string, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
561
|
+
} else if ($value.value === '') {
|
|
562
|
+
logger.error({ message: 'Expected URL, received empty string', node, ast, loc: getLoc($value) });
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'string': {
|
|
567
|
+
if ($value.type !== 'String') {
|
|
568
|
+
logger.error({ message: `Expected string, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// composite types
|
|
574
|
+
case 'border': {
|
|
575
|
+
validateBorder($value, node, { ast, logger });
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'gradient': {
|
|
579
|
+
validateGradient($value, node, { ast, logger });
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case 'strokeStyle': {
|
|
583
|
+
validateStrokeStyle($value, node, { ast, logger });
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case 'transition': {
|
|
587
|
+
validateTransition($value, node, { ast, logger });
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'typography': {
|
|
591
|
+
if ($value.type !== 'Object') {
|
|
592
|
+
logger.error({ message: `Expected object, received ${$value.type}`, node, ast, loc: getLoc($value) });
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if ($value.members.length === 0) {
|
|
596
|
+
logger.error({
|
|
597
|
+
message: 'Empty typography token. Must contain at least 1 property.',
|
|
598
|
+
node,
|
|
599
|
+
ast,
|
|
600
|
+
loc: getLoc($value),
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
validateMembersAs(
|
|
604
|
+
$value,
|
|
605
|
+
{
|
|
606
|
+
fontFamily: { validator: validateFontFamily },
|
|
607
|
+
fontWeight: { validator: validateFontWeight },
|
|
608
|
+
},
|
|
609
|
+
node,
|
|
610
|
+
{ ast, logger },
|
|
611
|
+
);
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
default: {
|
|
616
|
+
// noop
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
package/parse/yaml.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DocumentNode } from '@humanwhocodes/momoa';
|
|
2
|
+
import type Logger from '../logger.js';
|
|
3
|
+
|
|
4
|
+
export interface ParseYAMLOptions {
|
|
5
|
+
logger: Logger;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Take a YAML document and create a Momoa JSON AST from it
|
|
10
|
+
*/
|
|
11
|
+
export default function yamlToAST(input: string, options: ParseYAMLOptions): DocumentNode;
|
package/parse/yaml.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Composer, Parser } from 'yaml';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("yaml").YAMLError} YAMLError
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert YAML position to line, column
|
|
9
|
+
* @param {string} input
|
|
10
|
+
* @param {YAMLError{"pos"]} pos
|
|
11
|
+
* @return {import("@babel/code-frame").SourceLocation["start"]}
|
|
12
|
+
*/
|
|
13
|
+
function posToLoc(input, pos) {
|
|
14
|
+
let line = 1;
|
|
15
|
+
let column = 0;
|
|
16
|
+
for (let i = 0; i <= (pos[0] || 0); i++) {
|
|
17
|
+
const c = input[i];
|
|
18
|
+
if (c === '\n') {
|
|
19
|
+
line++;
|
|
20
|
+
column = 0;
|
|
21
|
+
}
|
|
22
|
+
column++;
|
|
23
|
+
}
|
|
24
|
+
return { line, column };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Take a YAML document and create a Momoa JSON AST from it
|
|
29
|
+
*/
|
|
30
|
+
export default function yamlToAST(input, { logger }) {
|
|
31
|
+
const parser = new Parser();
|
|
32
|
+
const composer = new Composer();
|
|
33
|
+
for (const node of composer.compose(parser.parse(input))) {
|
|
34
|
+
if (node.errors) {
|
|
35
|
+
for (const error of node.errors) {
|
|
36
|
+
logger.error({
|
|
37
|
+
label: 'parse:yaml',
|
|
38
|
+
message: `${error.code} ${error.message}`,
|
|
39
|
+
code: input,
|
|
40
|
+
loc: posToLoc(input, error.pos),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|