@terrazzo/parser 0.7.1 → 0.7.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 +18 -0
- package/README.md +35 -2
- package/dist/build/index.js +1 -1
- package/dist/build/index.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -2
- package/dist/logger.js.map +1 -1
- package/dist/parse/index.d.ts +7 -2
- package/dist/parse/index.d.ts.map +1 -1
- package/dist/parse/index.js +60 -13
- package/dist/parse/index.js.map +1 -1
- package/dist/parse/json.d.ts +8 -8
- package/dist/parse/json.d.ts.map +1 -1
- package/dist/parse/json.js +29 -22
- package/dist/parse/json.js.map +1 -1
- package/dist/parse/normalize.d.ts.map +1 -1
- package/dist/parse/normalize.js +4 -13
- package/dist/parse/normalize.js.map +1 -1
- package/dist/parse/validate.d.ts +31 -4
- package/dist/parse/validate.d.ts.map +1 -1
- package/dist/parse/validate.js +44 -55
- package/dist/parse/validate.js.map +1 -1
- package/package.json +3 -3
- package/src/logger.ts +4 -2
- package/src/parse/index.ts +72 -13
- package/src/parse/json.ts +35 -29
- package/src/parse/normalize.ts +4 -14
- package/src/parse/validate.ts +81 -68
package/src/parse/json.ts
CHANGED
|
@@ -3,15 +3,16 @@ import {
|
|
|
3
3
|
type DocumentNode,
|
|
4
4
|
type MemberNode,
|
|
5
5
|
type ObjectNode,
|
|
6
|
+
type ParseOptions,
|
|
6
7
|
type ValueNode,
|
|
7
|
-
parse as
|
|
8
|
+
parse as momoaParse,
|
|
8
9
|
print,
|
|
9
10
|
} from '@humanwhocodes/momoa';
|
|
10
11
|
import type yamlToMomoa from 'yaml-to-momoa';
|
|
11
12
|
import type Logger from '../logger.js';
|
|
12
13
|
import type { InputSource } from '../types.js';
|
|
13
14
|
|
|
14
|
-
export interface
|
|
15
|
+
export interface JSONVisitor {
|
|
15
16
|
enter?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
16
17
|
exit?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
17
18
|
}
|
|
@@ -54,11 +55,7 @@ export function getObjMembers(node: ObjectNode): Record<string | number, ValueNo
|
|
|
54
55
|
return members;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* Inject members to ObjectNode
|
|
59
|
-
* @param {ObjectNode} node
|
|
60
|
-
* @param {MemberNode[]} members
|
|
61
|
-
*/
|
|
58
|
+
/** Inject members to ObjectNode */
|
|
62
59
|
export function injectObjMembers(node: ObjectNode, members: MemberNode[] = []) {
|
|
63
60
|
if (node.type !== 'Object') {
|
|
64
61
|
return;
|
|
@@ -66,11 +63,16 @@ export function injectObjMembers(node: ObjectNode, members: MemberNode[] = []) {
|
|
|
66
63
|
node.members.push(...members);
|
|
67
64
|
}
|
|
68
65
|
|
|
66
|
+
/** Replace an ObjectNode’s contents outright with another */
|
|
67
|
+
export function replaceObjMembers(a: ObjectNode, b: DocumentNode | ObjectNode) {
|
|
68
|
+
a.members = (b.type === 'Document' && (b.body as ObjectNode)?.members) || (b as ObjectNode).members;
|
|
69
|
+
}
|
|
70
|
+
|
|
69
71
|
/**
|
|
70
72
|
* Variation of Momoa’s traverse(), which keeps track of global path.
|
|
71
73
|
* Allows mutation of AST (along with any consequences)
|
|
72
74
|
*/
|
|
73
|
-
export function traverse(root: AnyNode, visitor:
|
|
75
|
+
export function traverse(root: AnyNode, visitor: JSONVisitor) {
|
|
74
76
|
/**
|
|
75
77
|
* Recursively visits a node.
|
|
76
78
|
* @param {AnyNode} node The node to visit.
|
|
@@ -89,20 +91,15 @@ export function traverse(root: AnyNode, visitor: Visitor) {
|
|
|
89
91
|
const childNode = CHILD_KEYS[node.type];
|
|
90
92
|
for (const key of childNode ?? []) {
|
|
91
93
|
const value = node[key as keyof typeof node];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
value[i],
|
|
99
|
-
node,
|
|
100
|
-
key === 'elements' ? [...nextPath, String(i)] : nextPath,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
} else if (isNode(value)) {
|
|
104
|
-
visitNode(value as unknown as AnyNode, node, nextPath);
|
|
94
|
+
if (!value) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
for (let i = 0; i < value.length; i++) {
|
|
99
|
+
visitNode(value[i] as unknown as AnyNode, node, key === 'elements' ? [...nextPath, String(i)] : nextPath);
|
|
105
100
|
}
|
|
101
|
+
} else if (isNode(value)) {
|
|
102
|
+
visitNode(value as unknown as AnyNode, node, nextPath);
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
|
|
@@ -189,17 +186,26 @@ export function toMomoa(
|
|
|
189
186
|
});
|
|
190
187
|
}
|
|
191
188
|
} else {
|
|
192
|
-
document = parseJSON(
|
|
193
|
-
typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), // everything else: assert it’s JSON-serializable
|
|
194
|
-
{
|
|
195
|
-
mode: 'jsonc',
|
|
196
|
-
ranges: true,
|
|
197
|
-
tokens: true,
|
|
198
|
-
},
|
|
199
|
-
);
|
|
189
|
+
document = parseJSON(input);
|
|
200
190
|
}
|
|
201
191
|
if (!src) {
|
|
202
192
|
src = print(document, { indent: 2 });
|
|
203
193
|
}
|
|
204
194
|
return { src, document };
|
|
205
195
|
}
|
|
196
|
+
|
|
197
|
+
/** Momoa, just with default options pre-set */
|
|
198
|
+
export function parseJSON(input: string | Record<string, any>, options?: ParseOptions): any {
|
|
199
|
+
return momoaParse(
|
|
200
|
+
// note: it seems silly, at first glance, to have JSON.stringify() inside an actual JSON parser. But
|
|
201
|
+
// this provides a common interface to generate a Momoa AST for JSON created in-memory, which we already
|
|
202
|
+
// know is 100% valid because it’s already deserialized.
|
|
203
|
+
typeof input === 'string' ? input : JSON.stringify(input, undefined, 2),
|
|
204
|
+
{
|
|
205
|
+
mode: 'jsonc',
|
|
206
|
+
ranges: true,
|
|
207
|
+
tokens: true,
|
|
208
|
+
...options,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
}
|
package/src/parse/normalize.ts
CHANGED
|
@@ -34,9 +34,6 @@ export const FONT_WEIGHT_MAP = {
|
|
|
34
34
|
'ultra-black': 950,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
// Note: because we’re handling a lot of input values, the type inference gets lost.
|
|
38
|
-
// This file is expected to have a lot of `@ts-ignore` comments.
|
|
39
|
-
|
|
40
37
|
const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
|
|
41
38
|
|
|
42
39
|
/** Fill in defaults, and return predictable shapes for tokens */
|
|
@@ -140,14 +137,10 @@ export default function normalizeValue<T extends Token>(token: T): T['$value'] {
|
|
|
140
137
|
(layer) =>
|
|
141
138
|
({
|
|
142
139
|
color: normalizeValue({ $type: 'color', $value: layer.color }),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// @ts-ignore
|
|
148
|
-
blur: normalizeValue({ $type: 'dimension', $value: layer.blur ?? 0 }),
|
|
149
|
-
// @ts-ignore
|
|
150
|
-
spread: normalizeValue({ $type: 'dimension', $value: layer.spread ?? 0 }),
|
|
140
|
+
offsetX: normalizeValue({ $type: 'dimension', $value: layer.offsetX ?? { value: 0, unit: 'px' } }),
|
|
141
|
+
offsetY: normalizeValue({ $type: 'dimension', $value: layer.offsetY ?? { value: 0, unit: 'px' } }),
|
|
142
|
+
blur: normalizeValue({ $type: 'dimension', $value: layer.blur ?? { value: 0, unit: 'px' } }),
|
|
143
|
+
spread: normalizeValue({ $type: 'dimension', $value: layer.spread ?? { value: 0, unit: 'px' } }),
|
|
151
144
|
inset: layer.inset === true,
|
|
152
145
|
}) as ShadowValueNormalized,
|
|
153
146
|
);
|
|
@@ -163,11 +156,8 @@ export default function normalizeValue<T extends Token>(token: T): T['$value'] {
|
|
|
163
156
|
return token.$value;
|
|
164
157
|
}
|
|
165
158
|
return {
|
|
166
|
-
// @ts-ignore
|
|
167
159
|
duration: normalizeValue({ $type: 'duration', $value: token.$value.duration ?? 0 }),
|
|
168
|
-
// @ts-ignore
|
|
169
160
|
delay: normalizeValue({ $type: 'duration', $value: token.$value.delay ?? 0 }),
|
|
170
|
-
// @ts-ignore
|
|
171
161
|
timingFunction: normalizeValue({ $type: 'cubicBezier', $value: token.$value.timingFunction }),
|
|
172
162
|
} as TransitionValue;
|
|
173
163
|
}
|
package/src/parse/validate.ts
CHANGED
|
@@ -20,6 +20,29 @@ export interface ValidateOptions {
|
|
|
20
20
|
logger: Logger;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface Visitors {
|
|
24
|
+
color?: Visitor;
|
|
25
|
+
dimension?: Visitor;
|
|
26
|
+
fontFamily?: Visitor;
|
|
27
|
+
fontWeight?: Visitor;
|
|
28
|
+
duration?: Visitor;
|
|
29
|
+
cubicBezier?: Visitor;
|
|
30
|
+
number?: Visitor;
|
|
31
|
+
link?: Visitor;
|
|
32
|
+
boolean?: Visitor;
|
|
33
|
+
strokeStyle?: Visitor;
|
|
34
|
+
border?: Visitor;
|
|
35
|
+
transition?: Visitor;
|
|
36
|
+
shadow?: Visitor;
|
|
37
|
+
gradient?: Visitor;
|
|
38
|
+
typography?: Visitor;
|
|
39
|
+
root?: Visitor;
|
|
40
|
+
group?: Visitor;
|
|
41
|
+
[key: string]: Visitor | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Visitor = (json: any, path: string, ast: AnyNode) => any | undefined | null;
|
|
45
|
+
|
|
23
46
|
export const VALID_COLORSPACES = new Set([
|
|
24
47
|
'adobe-rgb',
|
|
25
48
|
'display-p3',
|
|
@@ -587,10 +610,7 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
|
|
|
587
610
|
if (node.type !== 'Member' && node.type !== 'Object') {
|
|
588
611
|
logger.error({
|
|
589
612
|
...baseMessage,
|
|
590
|
-
message: `Expected Object, received ${JSON.stringify(
|
|
591
|
-
// @ts-ignore Yes, TypeScript, this SHOULD be unexpected. This is why we’re validating.
|
|
592
|
-
node.type,
|
|
593
|
-
)}`,
|
|
613
|
+
message: `Expected Object, received ${JSON.stringify(node.type)}`,
|
|
594
614
|
});
|
|
595
615
|
}
|
|
596
616
|
|
|
@@ -748,14 +768,48 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
|
|
|
748
768
|
}
|
|
749
769
|
}
|
|
750
770
|
|
|
771
|
+
/** Return any token node with its inherited $type */
|
|
772
|
+
export function getInheritedType(
|
|
773
|
+
node: MemberNode,
|
|
774
|
+
{ subpath, $typeInheritance }: { subpath: string[]; $typeInheritance?: Record<string, MemberNode> },
|
|
775
|
+
): MemberNode | undefined {
|
|
776
|
+
if (node.value.type !== 'Object') {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// keep track of $types
|
|
781
|
+
const $type = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type');
|
|
782
|
+
const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
|
|
783
|
+
if ($typeInheritance && $type && !$value) {
|
|
784
|
+
$typeInheritance[subpath.join('.') || '.'] = $type;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const id = subpath.join('.');
|
|
788
|
+
|
|
789
|
+
// get parent type by taking the closest-scoped $type (length === closer)
|
|
790
|
+
let parent$type: MemberNode | undefined;
|
|
791
|
+
let longestPath = '';
|
|
792
|
+
for (const [k, v] of Object.entries($typeInheritance ?? {})) {
|
|
793
|
+
if (k === '.' || id.startsWith(k)) {
|
|
794
|
+
if (k.length > longestPath.length) {
|
|
795
|
+
parent$type = v;
|
|
796
|
+
longestPath = k;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return parent$type;
|
|
802
|
+
}
|
|
803
|
+
|
|
751
804
|
export interface ValidateTokenNodeOptions {
|
|
752
805
|
subpath: string[];
|
|
753
806
|
src: string;
|
|
754
807
|
filename: URL;
|
|
755
808
|
config: ConfigInit;
|
|
756
809
|
logger: Logger;
|
|
757
|
-
parent
|
|
758
|
-
|
|
810
|
+
parent: AnyNode | undefined;
|
|
811
|
+
transform?: Visitors;
|
|
812
|
+
inheritedTypeNode?: MemberNode;
|
|
759
813
|
}
|
|
760
814
|
|
|
761
815
|
/**
|
|
@@ -765,10 +819,8 @@ export interface ValidateTokenNodeOptions {
|
|
|
765
819
|
*/
|
|
766
820
|
export default function validateTokenNode(
|
|
767
821
|
node: MemberNode,
|
|
768
|
-
{ config, filename, logger, parent, src, subpath
|
|
822
|
+
{ config, filename, logger, parent, inheritedTypeNode, src, subpath }: ValidateTokenNodeOptions,
|
|
769
823
|
): TokenNormalized | undefined {
|
|
770
|
-
// const start = performance.now();
|
|
771
|
-
|
|
772
824
|
// don’t validate $value
|
|
773
825
|
if (subpath.includes('$value') || node.value.type !== 'Object') {
|
|
774
826
|
return;
|
|
@@ -776,12 +828,6 @@ export default function validateTokenNode(
|
|
|
776
828
|
|
|
777
829
|
const members = getObjMembers(node.value);
|
|
778
830
|
|
|
779
|
-
// keep track of $types
|
|
780
|
-
if ($typeInheritance && members.$type && members.$type.type === 'String' && !members.$value) {
|
|
781
|
-
// @ts-ignore
|
|
782
|
-
$typeInheritance[subpath.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
|
|
783
|
-
}
|
|
784
|
-
|
|
785
831
|
// don’t validate $extensions or $defs
|
|
786
832
|
if (!members.$value || subpath.includes('$extensions') || subpath.includes('$deps')) {
|
|
787
833
|
return;
|
|
@@ -800,29 +846,13 @@ export default function validateTokenNode(
|
|
|
800
846
|
});
|
|
801
847
|
}
|
|
802
848
|
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
let longestPath = '';
|
|
809
|
-
for (const [k, v] of Object.entries($typeInheritance ?? {})) {
|
|
810
|
-
if (k === '.' || id.startsWith(k)) {
|
|
811
|
-
if (k.length > longestPath.length) {
|
|
812
|
-
parent$type = v;
|
|
813
|
-
longestPath = k;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
if (parent$type && !members.$type) {
|
|
818
|
-
injectObjMembers(
|
|
819
|
-
// @ts-ignore
|
|
820
|
-
sourceNode.value,
|
|
821
|
-
[parent$type],
|
|
822
|
-
);
|
|
849
|
+
const nodeWithType = structuredClone(node);
|
|
850
|
+
let $type = (members.$type?.type === 'String' && members.$type.value) || undefined;
|
|
851
|
+
if (inheritedTypeNode && !members.$type) {
|
|
852
|
+
injectObjMembers(nodeWithType.value as ObjectNode, [inheritedTypeNode]);
|
|
853
|
+
$type = (inheritedTypeNode.value as StringNode).value;
|
|
823
854
|
}
|
|
824
|
-
|
|
825
|
-
validateTokenMemberNode(sourceNode, { filename, src, logger });
|
|
855
|
+
validateTokenMemberNode(nodeWithType, { filename, src, logger });
|
|
826
856
|
|
|
827
857
|
// All tokens must be valid, so we want to validate it up till this
|
|
828
858
|
// point. However, if we are ignoring this token (or respecting
|
|
@@ -833,47 +863,37 @@ export default function validateTokenNode(
|
|
|
833
863
|
}
|
|
834
864
|
|
|
835
865
|
const group: TokenNormalized['group'] = { id: splitID(id).group!, tokens: [] };
|
|
836
|
-
if (
|
|
837
|
-
group.$type =
|
|
838
|
-
// @ts-ignore
|
|
839
|
-
parent$type.value.value;
|
|
866
|
+
if (inheritedTypeNode && inheritedTypeNode.value.type === 'String') {
|
|
867
|
+
group.$type = inheritedTypeNode.value.value as Token['$type'];
|
|
840
868
|
}
|
|
841
869
|
// note: this will also include sibling tokens, so be selective about only accessing group-specific properties
|
|
842
|
-
const groupMembers = getObjMembers(
|
|
843
|
-
// @ts-ignore
|
|
844
|
-
parent,
|
|
845
|
-
);
|
|
870
|
+
const groupMembers = getObjMembers(parent as ObjectNode);
|
|
846
871
|
if (groupMembers.$description) {
|
|
847
872
|
group.$description = evaluate(groupMembers.$description) as string;
|
|
848
873
|
}
|
|
849
874
|
if (groupMembers.$extensions) {
|
|
850
875
|
group.$extensions = evaluate(groupMembers.$extensions) as Record<string, unknown>;
|
|
851
876
|
}
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
$type
|
|
855
|
-
|
|
856
|
-
$value: evaluate(members.$value),
|
|
877
|
+
const $value = evaluate(members.$value!);
|
|
878
|
+
const token = {
|
|
879
|
+
$type,
|
|
880
|
+
$value,
|
|
857
881
|
id,
|
|
858
|
-
// @ts-ignore
|
|
859
882
|
mode: {},
|
|
860
|
-
// @ts-ignore
|
|
861
883
|
originalValue: evaluate(node.value),
|
|
862
884
|
group,
|
|
863
885
|
source: {
|
|
864
|
-
loc: filename
|
|
865
|
-
|
|
866
|
-
node: sourceNode.value,
|
|
886
|
+
loc: filename?.href,
|
|
887
|
+
node: nodeWithType.value as ObjectNode,
|
|
867
888
|
},
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
if (members.$description?.value) {
|
|
871
|
-
// @ts-ignore
|
|
889
|
+
} as unknown as TokenNormalized;
|
|
890
|
+
if (members.$description?.type === 'String' && members.$description.value) {
|
|
872
891
|
token.$description = members.$description.value;
|
|
873
892
|
}
|
|
874
893
|
|
|
875
894
|
// handle modes
|
|
876
895
|
// note that circular refs are avoided here, such as not duplicating `modes`
|
|
896
|
+
const extensions = members.$extensions ? getObjMembers(members.$extensions as ObjectNode) : undefined;
|
|
877
897
|
const modeValues = extensions?.mode ? getObjMembers(extensions.mode as any) : {};
|
|
878
898
|
for (const mode of ['.', ...Object.keys(modeValues)]) {
|
|
879
899
|
const modeValue = mode === '.' ? token.$value : (evaluate((modeValues as any)[mode]) as any);
|
|
@@ -881,18 +901,11 @@ export default function validateTokenNode(
|
|
|
881
901
|
$value: modeValue,
|
|
882
902
|
originalValue: modeValue,
|
|
883
903
|
source: {
|
|
884
|
-
loc: filename
|
|
885
|
-
|
|
886
|
-
node: modeValues[mode],
|
|
904
|
+
loc: filename?.href,
|
|
905
|
+
node: modeValues[mode] as ObjectNode,
|
|
887
906
|
},
|
|
888
907
|
};
|
|
889
908
|
}
|
|
890
909
|
|
|
891
|
-
// logger.debug({
|
|
892
|
-
// message: `${token.id}: validateTokenNode`,
|
|
893
|
-
// group: 'parser', label: 'validate',
|
|
894
|
-
// label: 'validate',
|
|
895
|
-
// timing: performance.now() - start,
|
|
896
|
-
// });
|
|
897
910
|
return token;
|
|
898
911
|
}
|