@terrazzo/parser 0.3.2 → 0.3.4

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.
@@ -4,11 +4,13 @@ import {
4
4
  type ObjectNode,
5
5
  type StringNode,
6
6
  type ValueNode,
7
+ evaluate,
7
8
  print,
8
9
  } from '@humanwhocodes/momoa';
9
- import { isAlias } from '@terrazzo/token-tools';
10
+ import { type Token, type TokenNormalized, isAlias, isTokenMatch, splitID } from '@terrazzo/token-tools';
10
11
  import type Logger from '../logger.js';
11
- import { getObjMembers } from './json.js';
12
+ import type { ConfigInit } from '../types.js';
13
+ import { getObjMembers, injectObjMembers } from './json.js';
12
14
 
13
15
  const listFormat = new Intl.ListFormat('en-us', { type: 'disjunction' });
14
16
 
@@ -510,7 +512,7 @@ export function validateTransition($value: ValueNode, node: AnyNode, { filename,
510
512
  * object) to see if it’s a valid DTCG token or not. Keeping the parent key
511
513
  * really helps in debug messages.
512
514
  */
513
- export default function validate(node: MemberNode, { filename, src, logger }: ValidateOptions) {
515
+ export function validateTokenMemberNode(node: MemberNode, { filename, src, logger }: ValidateOptions) {
514
516
  if (node.type !== 'Member' && node.type !== 'Object') {
515
517
  logger.error({
516
518
  message: `Expected Object, received ${JSON.stringify(
@@ -658,3 +660,142 @@ export default function validate(node: MemberNode, { filename, src, logger }: Va
658
660
  }
659
661
  }
660
662
  }
663
+
664
+ export interface ValidateTokenNodeOptions {
665
+ subpath: string[];
666
+ src: string;
667
+ filename: URL;
668
+ config: ConfigInit;
669
+ logger: Logger;
670
+ parent?: AnyNode;
671
+ $typeInheritance?: Record<string, Token['$type']>;
672
+ }
673
+
674
+ export default function validateTokenNode(
675
+ node: MemberNode,
676
+ { config, filename, logger, parent, src, subpath, $typeInheritance }: ValidateTokenNodeOptions,
677
+ ): TokenNormalized | undefined {
678
+ // don’t validate $value
679
+ if (subpath.includes('$value') || node.value.type !== 'Object') {
680
+ return;
681
+ }
682
+
683
+ const members = getObjMembers(node.value);
684
+
685
+ // keep track of $types
686
+ if ($typeInheritance && members.$type && members.$type.type === 'String' && !members.$value) {
687
+ // @ts-ignore
688
+ $typeInheritance[subpath.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
689
+ }
690
+
691
+ // don’t validate $extensions or $defs
692
+ if (!members.$value || subpath.includes('$extensions') || subpath.includes('$deps')) {
693
+ return;
694
+ }
695
+
696
+ const id = subpath.join('.');
697
+
698
+ if (!subpath.includes('.$value') && members.value) {
699
+ logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
700
+ }
701
+
702
+ const extensions = members.$extensions ? getObjMembers(members.$extensions as ObjectNode) : undefined;
703
+ const sourceNode = structuredClone(node);
704
+
705
+ // get parent type by taking the closest-scoped $type (length === closer)
706
+ let parent$type: Token['$type'] | undefined;
707
+ let longestPath = '';
708
+ for (const [k, v] of Object.entries($typeInheritance ?? {})) {
709
+ if (k === '.' || id.startsWith(k)) {
710
+ if (k.length > longestPath.length) {
711
+ parent$type = v;
712
+ longestPath = k;
713
+ }
714
+ }
715
+ }
716
+ if (parent$type && !members.$type) {
717
+ sourceNode.value = injectObjMembers(
718
+ // @ts-ignore
719
+ sourceNode.value,
720
+ [parent$type],
721
+ );
722
+ }
723
+
724
+ validateTokenMemberNode(sourceNode, { filename, src, logger });
725
+
726
+ // All tokens must be valid, so we want to validate it up till this
727
+ // point. However, if we are ignoring this token (or respecting
728
+ // $deprecated, we can omit it from the output.
729
+ const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
730
+ if ((config.ignore.deprecated && $deprecated) || (config.ignore.tokens && isTokenMatch(id, config.ignore.tokens))) {
731
+ return;
732
+ }
733
+
734
+ const group: TokenNormalized['group'] = { id: splitID(id).group!, tokens: [] };
735
+ if (parent$type) {
736
+ group.$type =
737
+ // @ts-ignore
738
+ parent$type.value.value;
739
+ }
740
+ // note: this will also include sibling tokens, so be selective about only accessing group-specific properties
741
+ const groupMembers = getObjMembers(
742
+ // @ts-ignore
743
+ parent,
744
+ );
745
+ if (groupMembers.$description) {
746
+ group.$description = evaluate(groupMembers.$description) as string;
747
+ }
748
+ if (groupMembers.$extensions) {
749
+ group.$extensions = evaluate(groupMembers.$extensions) as Record<string, unknown>;
750
+ }
751
+ const token: TokenNormalized = {
752
+ // @ts-ignore
753
+ $type: members.$type?.value ?? parent$type?.value.value,
754
+ // @ts-ignore
755
+ $value: evaluate(members.$value),
756
+ id,
757
+ // @ts-ignore
758
+ mode: {},
759
+ // @ts-ignore
760
+ originalValue: evaluate(node.value),
761
+ group,
762
+ source: {
763
+ loc: filename ? filename.href : undefined,
764
+ // @ts-ignore
765
+ node: sourceNode.value,
766
+ },
767
+ };
768
+ // @ts-ignore
769
+ if (members.$description?.value) {
770
+ // @ts-ignore
771
+ token.$description = members.$description.value;
772
+ }
773
+
774
+ // handle modes
775
+ // note that circular refs are avoided here, such as not duplicating `modes`
776
+ const modeValues = extensions?.mode
777
+ ? getObjMembers(
778
+ // @ts-ignore
779
+ extensions.mode,
780
+ )
781
+ : {};
782
+ for (const mode of ['.', ...Object.keys(modeValues)]) {
783
+ token.mode[mode] = {
784
+ id: token.id,
785
+ // @ts-ignore
786
+ $type: token.$type,
787
+ // @ts-ignore
788
+ $value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
789
+ source: {
790
+ loc: filename ? filename.href : undefined,
791
+ // @ts-ignore
792
+ node: mode === '.' ? structuredClone(token.source.node) : modeValues[mode],
793
+ },
794
+ };
795
+ if (token.$description) {
796
+ token.mode[mode]!.$description = token.$description;
797
+ }
798
+ }
799
+
800
+ return token;
801
+ }