@terrazzo/parser 0.0.18 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +25 -0
- package/logger.js +6 -2
- package/package.json +5 -5
- package/parse/index.js +87 -13
- package/parse/normalize.js +17 -5
- package/parse/validate.d.ts +1 -1
- package/parse/validate.js +64 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @terrazzo/parser
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#319](https://github.com/terrazzoapp/terrazzo/pull/319) [`e7f272d`](https://github.com/terrazzoapp/terrazzo/commit/e7f272defcd889f5a410fdbd30497cf704671b32) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ Breaking change: dimension and duration tokens normalize to object syntax in plugins (following upcoming changes in DTCG spec; see https://github.com/design-tokens/community-group/pull/244).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`e7f272d`](https://github.com/terrazzoapp/terrazzo/commit/e7f272defcd889f5a410fdbd30497cf704671b32)]:
|
|
12
|
+
- @terrazzo/token-tools@0.1.0
|
|
13
|
+
|
|
14
|
+
## 0.0.19
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [#313](https://github.com/terrazzoapp/terrazzo/pull/313) [`1408594`](https://github.com/terrazzoapp/terrazzo/commit/1408594de029f57137c936dc2ff9ab949f039215) Thanks [@drwpow](https://github.com/drwpow)! - Fix bug in gradient position aliasing
|
|
19
|
+
|
|
20
|
+
- [#313](https://github.com/terrazzoapp/terrazzo/pull/313) [`1408594`](https://github.com/terrazzoapp/terrazzo/commit/1408594de029f57137c936dc2ff9ab949f039215) Thanks [@drwpow](https://github.com/drwpow)! - Improve alias type validation
|
|
21
|
+
|
|
3
22
|
## 0.0.18
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Contributing to @terrazzo/parser
|
|
2
|
+
|
|
3
|
+
This document contains developer notes and context for @terrazzo/parser. For general contribution guidelines, see [CONTRIBUTING.md](../../CONTRIBUTING.md) in the root.
|
|
4
|
+
|
|
5
|
+
## What this package does
|
|
6
|
+
|
|
7
|
+
- Parses, validates, and normalizes tokens.json
|
|
8
|
+
- Executes plugins and builds output
|
|
9
|
+
- Executes linting
|
|
10
|
+
|
|
11
|
+
## What this package DOES NOT do
|
|
12
|
+
|
|
13
|
+
- Write files to disk (that’s a job for Node.js; this can be run in a browser)
|
|
14
|
+
|
|
15
|
+
## Manual JS and DTS
|
|
16
|
+
|
|
17
|
+
This package is unique in its manual handling of `.js` and `.d.ts` files (TypeScript definitions). The expected norm is to write `.ts`, and have `.js` and `.d.ts` be generated automatically from source. But in this project, we write `.js` by hand because this specific module has several unique concerns:
|
|
18
|
+
|
|
19
|
+
1. **It’s dealing with external (possibly invalid) code.** Now, you’d think that `.ts` would be better at handling this, but the reality when dealing with user-submitted code is if you type it too strongly, TS lulls you into a false sense of security and misses necessary assertions. If you type it weakly, then TS requires _too many_ assertions and you end up with boilerplate and noise. Or if you don’t type it at all, then TS doesn’t really provide any value in the first place, and you might as well skip it.
|
|
20
|
+
|
|
21
|
+
2. **It’s dealing with ASTs.** We’re parsing JSON ASTs using Momoa, and building ad hoc structures on-the-fly. ASTs usually trip TS up because the discrimination isn’t clear (e.g. if `.type == 'foo'`, then `[property]` exists). Most ASTs don’t have perfect types out-of-the-box, and attempting to re-type an AST yourself is overkill. Further, to the previous point, we may be dealing with invalid code, and the AST may not always be in predictable shapes! When faced with these problems, TS at best requires extra boilerplate; at worst guides you into incorrect decisions.
|
|
22
|
+
|
|
23
|
+
Given both of these, you usually end up with so many `// @ts-ignore`s or `as any`s that TS doesn’t provide benefit.
|
|
24
|
+
|
|
25
|
+
But this package is unique! Look in any other package in this monorepo, and you’ll see it’s fully written in `.ts` source files. Different concerns require different approaches.
|
package/logger.js
CHANGED
|
@@ -25,8 +25,12 @@ export function formatMessage(entry, severity) {
|
|
|
25
25
|
}
|
|
26
26
|
if (entry.src) {
|
|
27
27
|
const start = entry.node?.loc?.start;
|
|
28
|
-
//
|
|
29
|
-
|
|
28
|
+
// strip "file://" protocol, but not href
|
|
29
|
+
const loc = entry.filename
|
|
30
|
+
? `${entry.filename?.href.replace(/^file:\/\//, '')}:${start?.line ?? 0}:${start?.column ?? 0}\n\n`
|
|
31
|
+
: '';
|
|
32
|
+
const codeFrame = codeFrameColumns(entry.src, { start }, { highlightCode: false });
|
|
33
|
+
message = `${message}\n\n${loc}${codeFrame}`;
|
|
30
34
|
}
|
|
31
35
|
return message;
|
|
32
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrazzo/parser",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@humanwhocodes/momoa": "^3.
|
|
33
|
+
"@humanwhocodes/momoa": "^3.3.0",
|
|
34
34
|
"@types/babel__code-frame": "^7.0.6",
|
|
35
35
|
"@types/culori": "^2.1.1",
|
|
36
36
|
"culori": "^4.0.1",
|
|
37
37
|
"is-what": "^4.1.16",
|
|
38
38
|
"merge-anything": "^5.1.7",
|
|
39
|
-
"picocolors": "^1.1.
|
|
39
|
+
"picocolors": "^1.1.1",
|
|
40
40
|
"wildcard-match": "^5.1.3",
|
|
41
|
-
"yaml": "^2.
|
|
42
|
-
"@terrazzo/token-tools": "^0.0
|
|
41
|
+
"yaml": "^2.6.0",
|
|
42
|
+
"@terrazzo/token-tools": "^0.1.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"esbuild": "^0.23.1",
|
package/parse/index.js
CHANGED
|
@@ -131,7 +131,12 @@ export default async function parse(
|
|
|
131
131
|
if (mode === '.') {
|
|
132
132
|
continue; // skip shadow of root value
|
|
133
133
|
}
|
|
134
|
-
applyAliases(tokens[id].mode[mode], {
|
|
134
|
+
applyAliases(tokens[id].mode[mode], {
|
|
135
|
+
tokens,
|
|
136
|
+
node: tokens[id].mode[mode].source.node,
|
|
137
|
+
logger,
|
|
138
|
+
src: sources[tokens[id].source.loc]?.src,
|
|
139
|
+
});
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
logger.debug({
|
|
@@ -235,10 +240,11 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
|
|
|
235
240
|
$typeInheritance[path.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
const id = path.join('.');
|
|
244
|
+
|
|
238
245
|
if (members.$value) {
|
|
239
246
|
const extensions = members.$extensions ? getObjMembers(members.$extensions) : undefined;
|
|
240
247
|
const sourceNode = structuredClone(node);
|
|
241
|
-
const id = path.join('.');
|
|
242
248
|
|
|
243
249
|
// get parent type by taking the closest-scoped $type (length === closer)
|
|
244
250
|
let parent$type;
|
|
@@ -304,7 +310,7 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
tokens[id] = token;
|
|
307
|
-
} else if (members.value) {
|
|
313
|
+
} else if (!id.includes('.$value') && members.value) {
|
|
308
314
|
logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
|
|
309
315
|
}
|
|
310
316
|
}
|
|
@@ -419,10 +425,10 @@ export function maybeJSONString(input) {
|
|
|
419
425
|
export function resolveAlias(alias, { tokens, logger, filename, src, node, scanned = [] }) {
|
|
420
426
|
const { id } = parseAlias(alias);
|
|
421
427
|
if (!tokens[id]) {
|
|
422
|
-
logger.error({ message: `Alias "${alias}" not found
|
|
428
|
+
logger.error({ message: `Alias "${alias}" not found.`, filename, src, node });
|
|
423
429
|
}
|
|
424
430
|
if (scanned.includes(id)) {
|
|
425
|
-
logger.error({ message: `Circular alias detected from "${alias}"
|
|
431
|
+
logger.error({ message: `Circular alias detected from "${alias}".`, filename, src, node });
|
|
426
432
|
}
|
|
427
433
|
const token = tokens[id];
|
|
428
434
|
if (!isAlias(token.$value)) {
|
|
@@ -431,8 +437,43 @@ export function resolveAlias(alias, { tokens, logger, filename, src, node, scann
|
|
|
431
437
|
return resolveAlias(token.$value, { tokens, logger, filename, node, src, scanned: [...scanned, id] });
|
|
432
438
|
}
|
|
433
439
|
|
|
440
|
+
/** Throw error if resolved alias for composite properties doesn’t match expected $type. */
|
|
441
|
+
const COMPOSITE_TYPE_VALUES = {
|
|
442
|
+
border: {
|
|
443
|
+
color: ['color'],
|
|
444
|
+
width: ['dimension'],
|
|
445
|
+
strokeStyle: ['strokeStyle'],
|
|
446
|
+
},
|
|
447
|
+
gradient: {
|
|
448
|
+
color: ['color'],
|
|
449
|
+
position: ['number'],
|
|
450
|
+
},
|
|
451
|
+
shadow: {
|
|
452
|
+
color: ['color'],
|
|
453
|
+
position: ['dimension'],
|
|
454
|
+
},
|
|
455
|
+
strokeStyle: {
|
|
456
|
+
dashArray: ['dimension'],
|
|
457
|
+
},
|
|
458
|
+
transition: {
|
|
459
|
+
duration: ['duration'],
|
|
460
|
+
delay: ['duration'],
|
|
461
|
+
timingFunction: ['cubicBezier'],
|
|
462
|
+
},
|
|
463
|
+
typography: {
|
|
464
|
+
fontFamily: ['fontFamily'],
|
|
465
|
+
fontSize: ['dimension'],
|
|
466
|
+
fontWeight: ['fontWeight'],
|
|
467
|
+
letterSpacing: ['dimension'],
|
|
468
|
+
lineHeight: ['dimension', 'number'],
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
|
|
434
472
|
/** Resolve aliases, update values, and mutate `token` to add `aliasOf` / `partialAliasOf` */
|
|
435
473
|
function applyAliases(token, { tokens, logger, filename, src, node }) {
|
|
474
|
+
const $valueNode = (node.type === 'Object' && node.members.find((m) => m.name.value === '$value')?.value) || node;
|
|
475
|
+
const expectedAliasTypes = COMPOSITE_TYPE_VALUES[token.$type];
|
|
476
|
+
|
|
436
477
|
// handle simple aliases
|
|
437
478
|
if (isAlias(token.$value)) {
|
|
438
479
|
const aliasOfID = resolveAlias(token.$value, { tokens, logger, filename, node, src });
|
|
@@ -441,15 +482,16 @@ function applyAliases(token, { tokens, logger, filename, src, node }) {
|
|
|
441
482
|
token.aliasOf = aliasOfID;
|
|
442
483
|
token.$value = aliasOf.mode[aliasMode]?.$value || aliasOf.$value;
|
|
443
484
|
if (token.$type && token.$type !== aliasOf.$type) {
|
|
444
|
-
logger.
|
|
445
|
-
message: `
|
|
446
|
-
node,
|
|
485
|
+
logger.error({
|
|
486
|
+
message: `Invalid alias: expected $type: "${token.$type}", received $type: "${aliasOf.$type}".`,
|
|
487
|
+
node: $valueNode,
|
|
488
|
+
filename,
|
|
489
|
+
src,
|
|
447
490
|
});
|
|
448
|
-
token.$type = aliasOf.$type;
|
|
449
|
-
} else {
|
|
450
|
-
token.$type = aliasOf.$type;
|
|
451
491
|
}
|
|
492
|
+
token.$type = aliasOf.$type;
|
|
452
493
|
}
|
|
494
|
+
|
|
453
495
|
// handle aliases within array values (e.g. cubicBezier, gradient)
|
|
454
496
|
else if (Array.isArray(token.$value)) {
|
|
455
497
|
// some arrays are primitives, some are objects. handle both
|
|
@@ -473,8 +515,20 @@ function applyAliases(token, { tokens, logger, filename, src, node }) {
|
|
|
473
515
|
}
|
|
474
516
|
const aliasOfID = resolveAlias(token.$value[i][property], { tokens, logger, filename, node, src });
|
|
475
517
|
const { id: aliasID, mode: aliasMode } = parseAlias(token.$value[i][property]);
|
|
518
|
+
const aliasToken = tokens[aliasOfID];
|
|
519
|
+
|
|
520
|
+
if (expectedAliasTypes?.[property] && !expectedAliasTypes[property].includes(aliasToken.$type)) {
|
|
521
|
+
const elementNode = $valueNode.elements[i].value;
|
|
522
|
+
logger.error({
|
|
523
|
+
message: `Invalid alias: expected $type: "${expectedAliasTypes[property].join('" or "')}", received $type: "${aliasToken.$type}".`,
|
|
524
|
+
node: elementNode.members.find((m) => m.name.value === property).value,
|
|
525
|
+
filename,
|
|
526
|
+
src,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
476
530
|
token.partialAliasOf[i][property] = aliasID; // also keep the shallow alias here, too!
|
|
477
|
-
token.$value[i][property] =
|
|
531
|
+
token.$value[i][property] = aliasToken.mode[aliasMode]?.$value || aliasToken.$value;
|
|
478
532
|
}
|
|
479
533
|
}
|
|
480
534
|
}
|
|
@@ -494,8 +548,18 @@ function applyAliases(token, { tokens, logger, filename, src, node }) {
|
|
|
494
548
|
const aliasOfID = resolveAlias(token.$value[property], { tokens, logger, filename, node, src });
|
|
495
549
|
const { id: aliasID, mode: aliasMode } = parseAlias(token.$value[property]);
|
|
496
550
|
token.partialAliasOf[property] = aliasID; // keep the shallow alias!
|
|
497
|
-
|
|
551
|
+
const aliasToken = tokens[aliasOfID];
|
|
552
|
+
if (expectedAliasTypes?.[property] && !expectedAliasTypes[property].includes(aliasToken.$type)) {
|
|
553
|
+
logger.error({
|
|
554
|
+
message: `Invalid alias: expected $type: "${expectedAliasTypes[property].join('" or "')}", received $type: "${aliasToken.$type}".`,
|
|
555
|
+
node: $valueNode.members.find((m) => m.name.value === property).value,
|
|
556
|
+
filename,
|
|
557
|
+
src,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
token.$value[property] = aliasToken.mode[aliasMode]?.$value || aliasToken.$value;
|
|
498
561
|
}
|
|
562
|
+
|
|
499
563
|
// strokeStyle has an array within an object
|
|
500
564
|
else if (Array.isArray(token.$value[property])) {
|
|
501
565
|
for (let i = 0; i < token.$value[property].length; i++) {
|
|
@@ -509,6 +573,16 @@ function applyAliases(token, { tokens, logger, filename, src, node }) {
|
|
|
509
573
|
}
|
|
510
574
|
const { id: aliasID, mode: aliasMode } = parseAlias(token.$value[property][i]);
|
|
511
575
|
token.partialAliasOf[property][i] = aliasID; // keep the shallow alias!
|
|
576
|
+
const aliasToken = tokens[aliasOfID];
|
|
577
|
+
if (expectedAliasTypes?.[property] && !expectedAliasTypes[property].includes(aliasToken.$type)) {
|
|
578
|
+
const arrayNode = $valueNode.members.find((m) => m.name.value === property).value;
|
|
579
|
+
logger.error({
|
|
580
|
+
message: `Invalid alias: expected $type: "${expectedAliasTypes[property].join('" or "')}", received $type: "${aliasToken.$type}".`,
|
|
581
|
+
node: arrayNode.elements[i],
|
|
582
|
+
filename,
|
|
583
|
+
src,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
512
586
|
token.$value[property][i] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
|
|
513
587
|
}
|
|
514
588
|
}
|
package/parse/normalize.js
CHANGED
|
@@ -21,6 +21,8 @@ export const FONT_WEIGHT_MAP = {
|
|
|
21
21
|
'ultra-black': 950,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
|
|
25
|
+
|
|
24
26
|
export default function normalizeValue(token) {
|
|
25
27
|
if (isAlias(token.$value)) {
|
|
26
28
|
return token.$value;
|
|
@@ -49,15 +51,25 @@ export default function normalizeValue(token) {
|
|
|
49
51
|
}
|
|
50
52
|
case 'dimension': {
|
|
51
53
|
if (token.$value === 0) {
|
|
52
|
-
return
|
|
54
|
+
return { value: 0, unit: 'px' };
|
|
55
|
+
}
|
|
56
|
+
// Backwards compat: handle string
|
|
57
|
+
if (typeof token.$value === 'string') {
|
|
58
|
+
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
|
|
59
|
+
return { value: Number.parseFloat(match?.[1] || token.$value), unit: match[2] || 'px' };
|
|
53
60
|
}
|
|
54
|
-
return
|
|
61
|
+
return token.$value;
|
|
55
62
|
}
|
|
56
63
|
case 'duration': {
|
|
57
64
|
if (token.$value === 0) {
|
|
58
|
-
return 0;
|
|
65
|
+
return { value: 0, unit: 'ms' };
|
|
59
66
|
}
|
|
60
|
-
|
|
67
|
+
// Backwards compat: handle string
|
|
68
|
+
if (typeof token.$value === 'string') {
|
|
69
|
+
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
|
|
70
|
+
return { value: Number.parseFloat(match?.[1] || token.$value), unit: match[2] || 'ms' };
|
|
71
|
+
}
|
|
72
|
+
return token.$value;
|
|
61
73
|
}
|
|
62
74
|
case 'fontFamily': {
|
|
63
75
|
return Array.isArray(token.$value) ? token.$value : [token.$value];
|
|
@@ -73,7 +85,7 @@ export default function normalizeValue(token) {
|
|
|
73
85
|
for (let i = 0; i < token.$value.length; i++) {
|
|
74
86
|
const stop = { ...token.$value[i] };
|
|
75
87
|
stop.color = normalizeValue({ $type: 'color', $value: stop.color });
|
|
76
|
-
if (
|
|
88
|
+
if (stop.position === undefined) {
|
|
77
89
|
stop.position = i / (token.$value.length - 1);
|
|
78
90
|
}
|
|
79
91
|
output.push(stop);
|
package/parse/validate.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface ValidateOptions {
|
|
|
12
12
|
logger: Logger;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function
|
|
15
|
+
export function validateAliasSyntax($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
16
16
|
|
|
17
17
|
export function validateBorder($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
18
18
|
|
package/parse/validate.js
CHANGED
|
@@ -92,7 +92,7 @@ function validateMembersAs($value, properties, node, { filename, src, logger })
|
|
|
92
92
|
}
|
|
93
93
|
const value = members[property];
|
|
94
94
|
if (isMaybeAlias(value)) {
|
|
95
|
-
|
|
95
|
+
validateAliasSyntax(value, node, { filename, src, logger });
|
|
96
96
|
} else {
|
|
97
97
|
validator(value, node, { filename, src, logger });
|
|
98
98
|
}
|
|
@@ -100,13 +100,13 @@ function validateMembersAs($value, properties, node, { filename, src, logger })
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Verify an Alias
|
|
103
|
+
* Verify an Alias $value is formatted correctly
|
|
104
104
|
* @param {ValueNode} $value
|
|
105
105
|
* @param {AnyNode} node
|
|
106
106
|
* @param {ValidateOptions} options
|
|
107
107
|
* @return {void}
|
|
108
108
|
*/
|
|
109
|
-
export function
|
|
109
|
+
export function validateAliasSyntax($value, node, { filename, src, logger }) {
|
|
110
110
|
if ($value.type !== 'String' || !isAlias($value.value)) {
|
|
111
111
|
logger.error({ message: `Invalid alias: ${print($value)}`, filename, node: $value, src });
|
|
112
112
|
}
|
|
@@ -262,13 +262,43 @@ export function validateDimension($value, node, { filename, src, logger }) {
|
|
|
262
262
|
if ($value.type === 'Number' && $value.value === 0) {
|
|
263
263
|
return; // `0` is a valid number
|
|
264
264
|
}
|
|
265
|
+
if ($value.type === 'Object') {
|
|
266
|
+
const { value, unit } = getObjMembers($value);
|
|
267
|
+
if (!value) {
|
|
268
|
+
logger.error({ message: 'Missing required property "value".', filename, node: $value, src });
|
|
269
|
+
}
|
|
270
|
+
if (!unit) {
|
|
271
|
+
logger.error({ message: 'Missing required property "unit".', filename, node: $value, src });
|
|
272
|
+
}
|
|
273
|
+
if (value.type !== 'Number') {
|
|
274
|
+
logger.error({ message: `Expected number, received ${value.type}`, filename, node: value, src });
|
|
275
|
+
}
|
|
276
|
+
if (!['px', 'em', 'rem'].includes(unit.value)) {
|
|
277
|
+
logger.error({
|
|
278
|
+
message: `Expected unit "px", "em", or "rem", received ${print(unit)}`,
|
|
279
|
+
filename,
|
|
280
|
+
node: unit,
|
|
281
|
+
src,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Backwards compat: string
|
|
265
287
|
if ($value.type !== 'String') {
|
|
266
288
|
logger.error({ message: `Expected string, received ${$value.type}`, filename, node: $value, src });
|
|
267
|
-
}
|
|
289
|
+
}
|
|
290
|
+
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
|
|
291
|
+
const unit = $value.value.replace(value, '');
|
|
292
|
+
if ($value.value === '') {
|
|
268
293
|
logger.error({ message: 'Expected dimension, received empty string', filename, node: $value, src });
|
|
269
|
-
} else if (
|
|
270
|
-
logger.error({
|
|
271
|
-
|
|
294
|
+
} else if (!['px', 'em', 'rem'].includes(unit)) {
|
|
295
|
+
logger.error({
|
|
296
|
+
message: `Expected unit "px", "em", or "rem", received ${JSON.stringify(unit || $value.value)}`,
|
|
297
|
+
filename,
|
|
298
|
+
node: $value,
|
|
299
|
+
src,
|
|
300
|
+
});
|
|
301
|
+
} else if (!Number.isFinite(Number.parseFloat(value))) {
|
|
272
302
|
logger.error({ message: `Expected dimension with units, received ${print($value)}`, filename, node: $value, src });
|
|
273
303
|
}
|
|
274
304
|
}
|
|
@@ -284,19 +314,39 @@ export function validateDuration($value, node, { filename, src, logger }) {
|
|
|
284
314
|
if ($value.type === 'Number' && $value.value === 0) {
|
|
285
315
|
return; // `0` is a valid number
|
|
286
316
|
}
|
|
317
|
+
if ($value.type === 'Object') {
|
|
318
|
+
const { value, unit } = getObjMembers($value);
|
|
319
|
+
if (!value) {
|
|
320
|
+
logger.error({ message: 'Missing required property "value".', filename, node: $value, src });
|
|
321
|
+
}
|
|
322
|
+
if (!unit) {
|
|
323
|
+
logger.error({ message: 'Missing required property "unit".', filename, node: $value, src });
|
|
324
|
+
}
|
|
325
|
+
if (value.type !== 'Number') {
|
|
326
|
+
logger.error({ message: `Expected number, received ${value.type}`, filename, node: value, src });
|
|
327
|
+
}
|
|
328
|
+
if (!['ms', 's'].includes(unit.value)) {
|
|
329
|
+
logger.error({ message: `Expected unit "ms" or "s", received ${print(unit)}`, filename, node: unit, src });
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Backwards compat: string
|
|
287
334
|
if ($value.type !== 'String') {
|
|
288
335
|
logger.error({ message: `Expected string, received ${$value.type}`, filename, node: $value, src });
|
|
289
|
-
}
|
|
336
|
+
}
|
|
337
|
+
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
|
|
338
|
+
const unit = $value.value.replace(value, '');
|
|
339
|
+
if ($value.value === '') {
|
|
290
340
|
logger.error({ message: 'Expected duration, received empty string', filename, node: $value, src });
|
|
291
|
-
} else if (
|
|
292
|
-
logger.error({ message: 'Missing unit "ms" or "s"', filename, node: $value, src });
|
|
293
|
-
} else if (!/^[0-9]+/.test($value.value)) {
|
|
341
|
+
} else if (!['ms', 's'].includes(unit)) {
|
|
294
342
|
logger.error({
|
|
295
|
-
message: `Expected
|
|
343
|
+
message: `Expected unit "ms" or "s", received ${JSON.stringify(unit || $value.value)}`,
|
|
296
344
|
filename,
|
|
297
345
|
node: $value,
|
|
298
346
|
src,
|
|
299
347
|
});
|
|
348
|
+
} else if (!Number.isFinite(Number.parseFloat(value))) {
|
|
349
|
+
logger.error({ message: `Expected duration with units, received ${print($value)}`, filename, node: $value, src });
|
|
300
350
|
}
|
|
301
351
|
}
|
|
302
352
|
|
|
@@ -476,7 +526,7 @@ export function validateStrokeStyle($value, node, { filename, src, logger }) {
|
|
|
476
526
|
for (const element of dashArray.elements) {
|
|
477
527
|
if (element.value.type === 'String' && element.value.value !== '') {
|
|
478
528
|
if (isMaybeAlias(element.value)) {
|
|
479
|
-
|
|
529
|
+
validateAliasSyntax(element.value, node, { logger, src });
|
|
480
530
|
} else {
|
|
481
531
|
validateDimension(element.value, node, { logger, src });
|
|
482
532
|
}
|
|
@@ -550,7 +600,7 @@ export default function validate(node, { filename, src, logger }) {
|
|
|
550
600
|
// If top-level value is a valid alias, this is valid (no need for $type)
|
|
551
601
|
// ⚠️ Important: ALL Object and Array nodes below will need to check for aliases within!
|
|
552
602
|
if (isMaybeAlias($value)) {
|
|
553
|
-
|
|
603
|
+
validateAliasSyntax($value, node, { logger, src });
|
|
554
604
|
return;
|
|
555
605
|
}
|
|
556
606
|
|