@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/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 parseJSON,
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 Visitor {
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: 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
- if (value && typeof value === 'object') {
94
- if (Array.isArray(value)) {
95
- for (let i = 0; i < value.length; i++) {
96
- visitNode(
97
- // @ts-expect-error this is safe
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
+ }
@@ -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
- // @ts-ignore
144
- offsetX: normalizeValue({ $type: 'dimension', $value: layer.offsetX ?? 0 }),
145
- // @ts-ignore
146
- offsetY: normalizeValue({ $type: 'dimension', $value: layer.offsetY ?? 0 }),
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
  }
@@ -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?: AnyNode;
758
- $typeInheritance?: Record<string, Token['$type']>;
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, $typeInheritance }: ValidateTokenNodeOptions,
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 extensions = members.$extensions ? getObjMembers(members.$extensions as ObjectNode) : undefined;
804
- const sourceNode = structuredClone(node);
805
-
806
- // get parent type by taking the closest-scoped $type (length === closer)
807
- let parent$type: Token['$type'] | undefined;
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 (parent$type) {
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 token: TokenNormalized = {
853
- // @ts-ignore
854
- $type: members.$type?.value ?? parent$type?.value.value,
855
- // @ts-ignore
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 ? filename.href : undefined,
865
- // @ts-ignore
866
- node: sourceNode.value,
886
+ loc: filename?.href,
887
+ node: nodeWithType.value as ObjectNode,
867
888
  },
868
- };
869
- // @ts-ignore
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 ? filename.href : undefined,
885
- // @ts-ignore
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
  }