@terrazzo/parser 0.10.1 → 0.10.3
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 +12 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/build/index.ts +5 -1
- package/src/config.ts +7 -2
- package/src/index.ts +17 -17
- package/src/lint/index.ts +1 -1
- package/src/parse/alias.ts +2 -2
- package/src/parse/index.ts +24 -13
- package/src/parse/json.ts +2 -2
- package/src/parse/normalize.ts +2 -2
- package/src/parse/validate.ts +66 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrazzo/parser",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"picocolors": "^1.1.1",
|
|
43
43
|
"scule": "^1.3.0",
|
|
44
44
|
"wildcard-match": "^5.1.4",
|
|
45
|
-
"@terrazzo/token-tools": "^0.10.
|
|
45
|
+
"@terrazzo/token-tools": "^0.10.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"yaml-to-momoa": "^0.0.3"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "rolldown -c && attw --profile esm-only --pack .",
|
|
52
|
-
"dev": "
|
|
52
|
+
"dev": "rolldown -w -c",
|
|
53
53
|
"lint": "pnpm --filter @terrazzo/parser run \"/^lint:(js|ts)/\"",
|
|
54
54
|
"lint:js": "biome check .",
|
|
55
55
|
"lint:ts": "tsc --noEmit",
|
package/src/build/index.ts
CHANGED
|
@@ -18,7 +18,11 @@ function validateTransformParams({
|
|
|
18
18
|
params,
|
|
19
19
|
logger,
|
|
20
20
|
pluginName,
|
|
21
|
-
}: {
|
|
21
|
+
}: {
|
|
22
|
+
params: TokenTransformed;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
pluginName: string;
|
|
25
|
+
}): void {
|
|
22
26
|
const baseMessage: LogEntry = { group: 'plugin', label: pluginName, message: '' };
|
|
23
27
|
|
|
24
28
|
// validate value is valid for SINGLE_VALUE or MULTI_VALUE
|
package/src/config.ts
CHANGED
|
@@ -48,7 +48,12 @@ function normalizeTokens({
|
|
|
48
48
|
config,
|
|
49
49
|
logger,
|
|
50
50
|
cwd,
|
|
51
|
-
}: {
|
|
51
|
+
}: {
|
|
52
|
+
rawConfig: Config;
|
|
53
|
+
config: ConfigInit;
|
|
54
|
+
logger: Logger;
|
|
55
|
+
cwd: URL;
|
|
56
|
+
}) {
|
|
52
57
|
if (rawConfig.tokens === undefined) {
|
|
53
58
|
config.tokens = [
|
|
54
59
|
// @ts-ignore we’ll normalize in next step
|
|
@@ -89,7 +94,7 @@ function normalizeTokens({
|
|
|
89
94
|
}
|
|
90
95
|
try {
|
|
91
96
|
config.tokens[i] = new URL(filepath, cwd);
|
|
92
|
-
} catch
|
|
97
|
+
} catch {
|
|
93
98
|
logger.error({ group: 'config', label: 'tokens', message: `Invalid URL ${filepath}` });
|
|
94
99
|
}
|
|
95
100
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
export { default as build } from './build/index.js';
|
|
2
|
-
export * from './build/index.js';
|
|
3
|
-
|
|
4
|
-
export { default as defineConfig } from './config.js';
|
|
5
|
-
export * from './config.js';
|
|
6
|
-
|
|
7
|
-
export { default as lintRunner } from './lint/index.js';
|
|
8
|
-
export * from './lint/index.js';
|
|
9
|
-
|
|
10
|
-
export { default as Logger } from './logger.js';
|
|
11
|
-
export * from './logger.js';
|
|
12
|
-
|
|
13
|
-
export { default as parse } from './parse/index.js';
|
|
14
|
-
export * from './parse/index.js';
|
|
15
|
-
|
|
16
|
-
export * from './types.js';
|
|
17
|
-
|
|
18
1
|
export type {
|
|
19
2
|
AliasToken,
|
|
20
3
|
AliasValue,
|
|
@@ -91,3 +74,20 @@ export type {
|
|
|
91
74
|
TypographyValue,
|
|
92
75
|
TypographyValueNormalized,
|
|
93
76
|
} from '@terrazzo/token-tools';
|
|
77
|
+
|
|
78
|
+
export * from './build/index.js';
|
|
79
|
+
export { default as build } from './build/index.js';
|
|
80
|
+
|
|
81
|
+
export * from './config.js';
|
|
82
|
+
export { default as defineConfig } from './config.js';
|
|
83
|
+
|
|
84
|
+
export * from './lint/index.js';
|
|
85
|
+
export { default as lintRunner } from './lint/index.js';
|
|
86
|
+
|
|
87
|
+
export * from './logger.js';
|
|
88
|
+
export { default as Logger } from './logger.js';
|
|
89
|
+
|
|
90
|
+
export * from './parse/index.js';
|
|
91
|
+
export { default as parse } from './parse/index.js';
|
|
92
|
+
|
|
93
|
+
export * from './types.js';
|
package/src/lint/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TokenNormalized
|
|
1
|
+
import { pluralize, type TokenNormalized } from '@terrazzo/token-tools';
|
|
2
2
|
import { merge } from 'merge-anything';
|
|
3
3
|
import type { LogEntry, default as Logger } from '../logger.js';
|
|
4
4
|
import type { ConfigInit } from '../types.js';
|
package/src/parse/alias.ts
CHANGED
|
@@ -2,14 +2,14 @@ import type { AnyNode, ArrayNode, ObjectNode } from '@humanwhocodes/momoa';
|
|
|
2
2
|
import {
|
|
3
3
|
type BorderTokenNormalized,
|
|
4
4
|
type GradientTokenNormalized,
|
|
5
|
+
isAlias,
|
|
6
|
+
parseAlias,
|
|
5
7
|
type ShadowTokenNormalized,
|
|
6
8
|
type StrokeStyleTokenNormalized,
|
|
7
9
|
type TokenNormalized,
|
|
8
10
|
type TokenNormalizedSet,
|
|
9
11
|
type TransitionTokenNormalized,
|
|
10
12
|
type TypographyTokenNormalized,
|
|
11
|
-
isAlias,
|
|
12
|
-
parseAlias,
|
|
13
13
|
} from '@terrazzo/token-tools';
|
|
14
14
|
import type Logger from '../logger.js';
|
|
15
15
|
import { getObjMembers } from './json.js';
|
package/src/parse/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type DocumentNode, type MemberNode, type ObjectNode
|
|
2
|
-
import { type Token, type TokenNormalized
|
|
1
|
+
import { type DocumentNode, evaluate, type MemberNode, type ObjectNode } from '@humanwhocodes/momoa';
|
|
2
|
+
import { pluralize, splitID, type Token, type TokenNormalized } from '@terrazzo/token-tools';
|
|
3
3
|
import type ytm from 'yaml-to-momoa';
|
|
4
4
|
import lintRunner from '../lint/index.js';
|
|
5
5
|
import Logger from '../logger.js';
|
|
@@ -7,11 +7,11 @@ import type { ConfigInit, InputSource } from '../types.js';
|
|
|
7
7
|
import applyAliases from './alias.js';
|
|
8
8
|
import { getObjMembers, parseJSON, replaceObjMembers, toMomoa, traverse } from './json.js';
|
|
9
9
|
import normalize from './normalize.js';
|
|
10
|
-
import validateTokenNode, { type Visitors
|
|
10
|
+
import validateTokenNode, { computeInheritedProperty, isGroupNode, type Visitors } from './validate.js';
|
|
11
11
|
|
|
12
12
|
export * from './alias.js';
|
|
13
|
-
export * from './normalize.js';
|
|
14
13
|
export * from './json.js';
|
|
14
|
+
export * from './normalize.js';
|
|
15
15
|
export * from './validate.js';
|
|
16
16
|
export { normalize, validateTokenNode };
|
|
17
17
|
|
|
@@ -210,16 +210,24 @@ async function parseSingle(
|
|
|
210
210
|
// 2. Walk AST to validate tokens
|
|
211
211
|
let tokenCount = 0;
|
|
212
212
|
const startValidate = performance.now();
|
|
213
|
+
// $type and $deprecated can be applied at group level to target all child tokens,
|
|
214
|
+
// these two objects keep track of inherited prop values as we traverse the token tree
|
|
213
215
|
const $typeInheritance: Record<string, MemberNode> = {};
|
|
216
|
+
const $deprecatedInheritance: Record<string, MemberNode> = {};
|
|
214
217
|
traverse(document, {
|
|
215
218
|
enter(node, parent, subpath) {
|
|
216
219
|
// if $type appears at root of tokens.json, collect it
|
|
217
220
|
if (node.type === 'Document' && node.body.type === 'Object' && node.body.members) {
|
|
218
221
|
const { members: rootMembers } = node.body;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
if (isGroupNode(node.body)) {
|
|
223
|
+
const root$type = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$type');
|
|
224
|
+
if (root$type) {
|
|
225
|
+
$typeInheritance['.'] = root$type;
|
|
226
|
+
}
|
|
227
|
+
const root$deprecated = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$deprecated');
|
|
228
|
+
if (root$deprecated) {
|
|
229
|
+
$deprecatedInheritance['.'] = root$deprecated;
|
|
230
|
+
}
|
|
223
231
|
}
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -241,13 +249,15 @@ async function parseSingle(
|
|
|
241
249
|
|
|
242
250
|
// handle tokens
|
|
243
251
|
if (node.type === 'Member') {
|
|
244
|
-
const
|
|
252
|
+
const inheritedDeprecatedNode = computeInheritedProperty(node, '$deprecated', {
|
|
253
|
+
subpath,
|
|
254
|
+
inherited: $deprecatedInheritance,
|
|
255
|
+
});
|
|
256
|
+
const inheritedTypeNode = computeInheritedProperty(node, '$type', { subpath, inherited: $typeInheritance });
|
|
245
257
|
if (node.value.type === 'Object') {
|
|
246
|
-
const $type =
|
|
247
|
-
node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type') || inheritedTypeNode;
|
|
248
258
|
const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
|
|
249
|
-
if ($value &&
|
|
250
|
-
const result = transform[
|
|
259
|
+
if ($value && inheritedTypeNode?.value.type === 'String' && transform?.[inheritedTypeNode.value.value]) {
|
|
260
|
+
const result = transform[inheritedTypeNode.value.value]?.(evaluate(node.value), subpath.join('.'), node);
|
|
251
261
|
if (result) {
|
|
252
262
|
node.value = parseJSON(result).body;
|
|
253
263
|
}
|
|
@@ -261,6 +271,7 @@ async function parseSingle(
|
|
|
261
271
|
parent,
|
|
262
272
|
subpath,
|
|
263
273
|
transform,
|
|
274
|
+
inheritedDeprecatedNode,
|
|
264
275
|
inheritedTypeNode,
|
|
265
276
|
});
|
|
266
277
|
if (token) {
|
package/src/parse/json.ts
CHANGED
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type DocumentNode,
|
|
4
4
|
type MemberNode,
|
|
5
|
+
parse as momoaParse,
|
|
5
6
|
type ObjectNode,
|
|
6
7
|
type ParseOptions,
|
|
7
|
-
type ValueNode,
|
|
8
|
-
parse as momoaParse,
|
|
9
8
|
print,
|
|
9
|
+
type ValueNode,
|
|
10
10
|
} from '@humanwhocodes/momoa';
|
|
11
11
|
import type yamlToMomoa from 'yaml-to-momoa';
|
|
12
12
|
import type Logger from '../logger.js';
|
package/src/parse/normalize.ts
CHANGED
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
type FontFamilyValue,
|
|
6
6
|
type GradientStopNormalized,
|
|
7
7
|
type GradientValueNormalized,
|
|
8
|
+
isAlias,
|
|
9
|
+
parseColor,
|
|
8
10
|
type ShadowValueNormalized,
|
|
9
11
|
type Token,
|
|
10
12
|
type TransitionValue,
|
|
11
13
|
type TypographyValueNormalized,
|
|
12
|
-
isAlias,
|
|
13
|
-
parseColor,
|
|
14
14
|
} from '@terrazzo/token-tools';
|
|
15
15
|
|
|
16
16
|
export const FONT_WEIGHT_MAP = {
|
package/src/parse/validate.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
|
+
type BooleanNode,
|
|
4
|
+
evaluate,
|
|
3
5
|
type MemberNode,
|
|
4
6
|
type ObjectNode,
|
|
7
|
+
print,
|
|
5
8
|
type StringNode,
|
|
6
9
|
type ValueNode,
|
|
7
|
-
evaluate,
|
|
8
|
-
print,
|
|
9
10
|
} from '@humanwhocodes/momoa';
|
|
10
|
-
import { type Token, type TokenNormalized
|
|
11
|
+
import { isAlias, splitID, type Token, type TokenNormalized } from '@terrazzo/token-tools';
|
|
11
12
|
import wcmatch from 'wildcard-match';
|
|
12
13
|
import type Logger from '../logger.js';
|
|
13
14
|
import type { ConfigInit } from '../types.js';
|
|
@@ -559,10 +560,12 @@ export function validateStrokeStyle($value: ValueNode, node: AnyNode, { filename
|
|
|
559
560
|
} else {
|
|
560
561
|
validateDimension(element.value, node, { logger, src });
|
|
561
562
|
}
|
|
563
|
+
} else if (element.value.type === 'Object') {
|
|
564
|
+
validateDimension(element.value, node, { logger, src });
|
|
562
565
|
} else {
|
|
563
566
|
logger.error({
|
|
564
567
|
...baseMessage,
|
|
565
|
-
message:
|
|
568
|
+
message: `Expected array of dimensions, received ${element.value.type}.`,
|
|
566
569
|
node: element,
|
|
567
570
|
});
|
|
568
571
|
}
|
|
@@ -769,28 +772,47 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
|
|
|
769
772
|
}
|
|
770
773
|
}
|
|
771
774
|
|
|
772
|
-
/** Return
|
|
773
|
-
|
|
775
|
+
/** Return whether a MemberNode is a group (has no `$value`).
|
|
776
|
+
* Groups can have properties that their child nodes will inherit. */
|
|
777
|
+
export function isGroupNode(node: ObjectNode): boolean {
|
|
778
|
+
if (node.type !== 'Object') {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// check for $value
|
|
783
|
+
const has$value = node.members.some((m) => m.name.type === 'String' && m.name.value === '$value');
|
|
784
|
+
return !has$value;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** Check if a token node has the specified property name, and if it does, stores
|
|
788
|
+
* the value in the `inherited` object as a side effect for future use. If not,
|
|
789
|
+
* traverses the `inherited` object to find the closest parent that has the property.
|
|
790
|
+
*
|
|
791
|
+
* Returns the property value if found locally or in a parent, otherwise undefined. */
|
|
792
|
+
export function computeInheritedProperty(
|
|
774
793
|
node: MemberNode,
|
|
775
|
-
|
|
794
|
+
propertyName: string,
|
|
795
|
+
{ subpath, inherited }: { subpath: string[]; inherited?: Record<string, MemberNode> },
|
|
776
796
|
): MemberNode | undefined {
|
|
777
797
|
if (node.value.type !== 'Object') {
|
|
778
798
|
return;
|
|
779
799
|
}
|
|
780
800
|
|
|
781
|
-
//
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
801
|
+
// if property exists locally in the token node, add it to the inherited tree
|
|
802
|
+
const property = node.value.members.find((m) => m.name.type === 'String' && m.name.value === propertyName);
|
|
803
|
+
if (inherited && property && isGroupNode(node.value)) {
|
|
804
|
+
// this is where the side effect occurs
|
|
805
|
+
inherited[subpath.join('.') || '.'] = property;
|
|
787
806
|
|
|
788
|
-
|
|
807
|
+
// We know this is the closest property, so return early
|
|
808
|
+
return property;
|
|
809
|
+
}
|
|
789
810
|
|
|
790
811
|
// get parent type by taking the closest-scoped $type (length === closer)
|
|
812
|
+
const id = subpath.join('.');
|
|
791
813
|
let parent$type: MemberNode | undefined;
|
|
792
814
|
let longestPath = '';
|
|
793
|
-
for (const [k, v] of Object.entries(
|
|
815
|
+
for (const [k, v] of Object.entries(inherited ?? {})) {
|
|
794
816
|
if (k === '.' || id.startsWith(k)) {
|
|
795
817
|
if (k.length > longestPath.length) {
|
|
796
818
|
parent$type = v;
|
|
@@ -810,6 +832,7 @@ export interface ValidateTokenNodeOptions {
|
|
|
810
832
|
logger: Logger;
|
|
811
833
|
parent: AnyNode | undefined;
|
|
812
834
|
transform?: Visitors;
|
|
835
|
+
inheritedDeprecatedNode?: MemberNode;
|
|
813
836
|
inheritedTypeNode?: MemberNode;
|
|
814
837
|
}
|
|
815
838
|
|
|
@@ -820,7 +843,16 @@ export interface ValidateTokenNodeOptions {
|
|
|
820
843
|
*/
|
|
821
844
|
export default function validateTokenNode(
|
|
822
845
|
node: MemberNode,
|
|
823
|
-
{
|
|
846
|
+
{
|
|
847
|
+
config,
|
|
848
|
+
filename,
|
|
849
|
+
logger,
|
|
850
|
+
parent,
|
|
851
|
+
inheritedDeprecatedNode,
|
|
852
|
+
inheritedTypeNode,
|
|
853
|
+
src,
|
|
854
|
+
subpath,
|
|
855
|
+
}: ValidateTokenNodeOptions,
|
|
824
856
|
): TokenNormalized | undefined {
|
|
825
857
|
// don’t validate $value
|
|
826
858
|
if (subpath.includes('$value') || node.value.type !== 'Object') {
|
|
@@ -848,17 +880,33 @@ export default function validateTokenNode(
|
|
|
848
880
|
}
|
|
849
881
|
|
|
850
882
|
const nodeWithType = structuredClone(node);
|
|
883
|
+
// inject $type that can be inherited in the DTCG format
|
|
851
884
|
let $type = (members.$type?.type === 'String' && members.$type.value) || undefined;
|
|
852
885
|
if (inheritedTypeNode && !members.$type) {
|
|
853
886
|
injectObjMembers(nodeWithType.value as ObjectNode, [inheritedTypeNode]);
|
|
854
887
|
$type = (inheritedTypeNode.value as StringNode).value;
|
|
855
888
|
}
|
|
889
|
+
|
|
890
|
+
// inject $deprecated that can also be inherited
|
|
891
|
+
let $deprecated = members.$deprecated
|
|
892
|
+
? members.$deprecated?.type === 'String'
|
|
893
|
+
? (members.$deprecated as StringNode).value
|
|
894
|
+
: (members.$deprecated as BooleanNode).value
|
|
895
|
+
: undefined;
|
|
896
|
+
if (inheritedDeprecatedNode && !members.$deprecated) {
|
|
897
|
+
injectObjMembers(nodeWithType.value as ObjectNode, [inheritedDeprecatedNode]);
|
|
898
|
+
$deprecated =
|
|
899
|
+
inheritedDeprecatedNode.value.type === 'String'
|
|
900
|
+
? (inheritedDeprecatedNode.value as StringNode).value
|
|
901
|
+
: (inheritedDeprecatedNode.value as BooleanNode).value;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// validate once after injecting all inherited properties
|
|
856
905
|
validateTokenMemberNode(nodeWithType, { filename, src, logger });
|
|
857
906
|
|
|
858
907
|
// All tokens must be valid, so we want to validate it up till this
|
|
859
908
|
// point. However, if we are ignoring this token (or respecting
|
|
860
909
|
// $deprecated, we can omit it from the output.
|
|
861
|
-
const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
|
|
862
910
|
if ((config.ignore.deprecated && $deprecated) || (config.ignore.tokens && wcmatch(config.ignore.tokens)(id))) {
|
|
863
911
|
return;
|
|
864
912
|
}
|
|
@@ -879,6 +927,7 @@ export default function validateTokenNode(
|
|
|
879
927
|
const token = {
|
|
880
928
|
$type,
|
|
881
929
|
$value,
|
|
930
|
+
$deprecated,
|
|
882
931
|
id,
|
|
883
932
|
mode: {},
|
|
884
933
|
originalValue: evaluate(node.value),
|