@terrazzo/parser 0.10.2 → 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 +6 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/parse/index.ts +21 -10
- package/src/parse/validate.ts +60 -13
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/parse/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ 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, {
|
|
10
|
+
import validateTokenNode, { computeInheritedProperty, isGroupNode, type Visitors } from './validate.js';
|
|
11
11
|
|
|
12
12
|
export * from './alias.js';
|
|
13
13
|
export * from './json.js';
|
|
@@ -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/validate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
|
+
type BooleanNode,
|
|
3
4
|
evaluate,
|
|
4
5
|
type MemberNode,
|
|
5
6
|
type ObjectNode,
|
|
@@ -771,28 +772,47 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
|
|
|
771
772
|
}
|
|
772
773
|
}
|
|
773
774
|
|
|
774
|
-
/** Return
|
|
775
|
-
|
|
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(
|
|
776
793
|
node: MemberNode,
|
|
777
|
-
|
|
794
|
+
propertyName: string,
|
|
795
|
+
{ subpath, inherited }: { subpath: string[]; inherited?: Record<string, MemberNode> },
|
|
778
796
|
): MemberNode | undefined {
|
|
779
797
|
if (node.value.type !== 'Object') {
|
|
780
798
|
return;
|
|
781
799
|
}
|
|
782
800
|
|
|
783
|
-
//
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
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;
|
|
789
806
|
|
|
790
|
-
|
|
807
|
+
// We know this is the closest property, so return early
|
|
808
|
+
return property;
|
|
809
|
+
}
|
|
791
810
|
|
|
792
811
|
// get parent type by taking the closest-scoped $type (length === closer)
|
|
812
|
+
const id = subpath.join('.');
|
|
793
813
|
let parent$type: MemberNode | undefined;
|
|
794
814
|
let longestPath = '';
|
|
795
|
-
for (const [k, v] of Object.entries(
|
|
815
|
+
for (const [k, v] of Object.entries(inherited ?? {})) {
|
|
796
816
|
if (k === '.' || id.startsWith(k)) {
|
|
797
817
|
if (k.length > longestPath.length) {
|
|
798
818
|
parent$type = v;
|
|
@@ -812,6 +832,7 @@ export interface ValidateTokenNodeOptions {
|
|
|
812
832
|
logger: Logger;
|
|
813
833
|
parent: AnyNode | undefined;
|
|
814
834
|
transform?: Visitors;
|
|
835
|
+
inheritedDeprecatedNode?: MemberNode;
|
|
815
836
|
inheritedTypeNode?: MemberNode;
|
|
816
837
|
}
|
|
817
838
|
|
|
@@ -822,7 +843,16 @@ export interface ValidateTokenNodeOptions {
|
|
|
822
843
|
*/
|
|
823
844
|
export default function validateTokenNode(
|
|
824
845
|
node: MemberNode,
|
|
825
|
-
{
|
|
846
|
+
{
|
|
847
|
+
config,
|
|
848
|
+
filename,
|
|
849
|
+
logger,
|
|
850
|
+
parent,
|
|
851
|
+
inheritedDeprecatedNode,
|
|
852
|
+
inheritedTypeNode,
|
|
853
|
+
src,
|
|
854
|
+
subpath,
|
|
855
|
+
}: ValidateTokenNodeOptions,
|
|
826
856
|
): TokenNormalized | undefined {
|
|
827
857
|
// don’t validate $value
|
|
828
858
|
if (subpath.includes('$value') || node.value.type !== 'Object') {
|
|
@@ -850,17 +880,33 @@ export default function validateTokenNode(
|
|
|
850
880
|
}
|
|
851
881
|
|
|
852
882
|
const nodeWithType = structuredClone(node);
|
|
883
|
+
// inject $type that can be inherited in the DTCG format
|
|
853
884
|
let $type = (members.$type?.type === 'String' && members.$type.value) || undefined;
|
|
854
885
|
if (inheritedTypeNode && !members.$type) {
|
|
855
886
|
injectObjMembers(nodeWithType.value as ObjectNode, [inheritedTypeNode]);
|
|
856
887
|
$type = (inheritedTypeNode.value as StringNode).value;
|
|
857
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
|
|
858
905
|
validateTokenMemberNode(nodeWithType, { filename, src, logger });
|
|
859
906
|
|
|
860
907
|
// All tokens must be valid, so we want to validate it up till this
|
|
861
908
|
// point. However, if we are ignoring this token (or respecting
|
|
862
909
|
// $deprecated, we can omit it from the output.
|
|
863
|
-
const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
|
|
864
910
|
if ((config.ignore.deprecated && $deprecated) || (config.ignore.tokens && wcmatch(config.ignore.tokens)(id))) {
|
|
865
911
|
return;
|
|
866
912
|
}
|
|
@@ -881,6 +927,7 @@ export default function validateTokenNode(
|
|
|
881
927
|
const token = {
|
|
882
928
|
$type,
|
|
883
929
|
$value,
|
|
930
|
+
$deprecated,
|
|
884
931
|
id,
|
|
885
932
|
mode: {},
|
|
886
933
|
originalValue: evaluate(node.value),
|