@terrazzo/parser 2.0.0-alpha.5 → 2.0.0-alpha.7
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/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +213 -94
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/lint/plugin-core/rules/valid-border.ts +1 -1
- package/src/lint/plugin-core/rules/valid-color.ts +1 -1
- package/src/lint/plugin-core/rules/valid-font-weight.ts +1 -1
- package/src/lint/plugin-core/rules/valid-shadow.ts +1 -1
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +3 -3
- package/src/lint/plugin-core/rules/valid-transition.ts +1 -1
- package/src/parse/load.ts +9 -8
- package/src/parse/process.ts +144 -16
- package/src/parse/token.ts +24 -10
- package/src/resolver/load.ts +0 -1
- package/src/resolver/normalize.ts +106 -79
- package/src/types.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrazzo/parser",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.7",
|
|
4
4
|
"description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
44
|
"scule": "^1.3.0",
|
|
45
45
|
"wildcard-match": "^5.1.4",
|
|
46
|
-
"@terrazzo/json-schema-tools": "^0.
|
|
47
|
-
"@terrazzo/token-tools": "^2.0.0-alpha.
|
|
46
|
+
"@terrazzo/json-schema-tools": "^0.2.0",
|
|
47
|
+
"@terrazzo/token-tools": "^2.0.0-alpha.7"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"yaml-to-momoa": "0.0.8"
|
|
@@ -12,7 +12,7 @@ const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
|
12
12
|
const rule: LintRule<typeof ERROR | typeof ERROR_INVALID_PROP, {}> = {
|
|
13
13
|
meta: {
|
|
14
14
|
messages: {
|
|
15
|
-
[ERROR]: `Border token missing required properties: ${new Intl.ListFormat(
|
|
15
|
+
[ERROR]: `Border token missing required properties: ${new Intl.ListFormat('en-us', { type: 'conjunction' }).format(BORDER_REQUIRED_PROPERTIES)}.`,
|
|
16
16
|
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
17
17
|
},
|
|
18
18
|
docs: {
|
|
@@ -54,7 +54,7 @@ const rule: LintRule<
|
|
|
54
54
|
meta: {
|
|
55
55
|
messages: {
|
|
56
56
|
[ERROR_ALPHA]: `Alpha {{ alpha }} not in range 0 – 1.`,
|
|
57
|
-
[ERROR_INVALID_COLOR_SPACE]: `Invalid color space: {{ colorSpace }}. Expected ${new Intl.ListFormat(
|
|
57
|
+
[ERROR_INVALID_COLOR_SPACE]: `Invalid color space: {{ colorSpace }}. Expected ${new Intl.ListFormat('en-us', { type: 'disjunction' }).format(Object.keys(COLORSPACE))}`,
|
|
58
58
|
[ERROR_INVALID_COLOR]: `Could not parse color {{ color }}.`,
|
|
59
59
|
[ERROR_INVALID_COMPONENT_LENGTH]: 'Expected {{ expected }} components, received {{ got }}.',
|
|
60
60
|
[ERROR_INVALID_HEX8]: `Hex value can’t be semi-transparent.`,
|
|
@@ -21,7 +21,7 @@ export interface RuleFontWeightOptions {
|
|
|
21
21
|
const rule: LintRule<typeof ERROR | typeof ERROR_STYLE, RuleFontWeightOptions> = {
|
|
22
22
|
meta: {
|
|
23
23
|
messages: {
|
|
24
|
-
[ERROR]: `Must either be a valid number (0 - 999) or a valid font weight: ${new Intl.ListFormat(
|
|
24
|
+
[ERROR]: `Must either be a valid number (0 - 999) or a valid font weight: ${new Intl.ListFormat('en-us', { type: 'disjunction' }).format(Object.keys(FONT_WEIGHTS))}.`,
|
|
25
25
|
[ERROR_STYLE]: 'Expected style {{ style }}, received {{ value }}.',
|
|
26
26
|
},
|
|
27
27
|
docs: {
|
|
@@ -12,7 +12,7 @@ const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
|
12
12
|
const rule: LintRule<typeof ERROR | typeof ERROR_INVALID_PROP> = {
|
|
13
13
|
meta: {
|
|
14
14
|
messages: {
|
|
15
|
-
[ERROR]: `Missing required properties: ${new Intl.ListFormat(
|
|
15
|
+
[ERROR]: `Missing required properties: ${new Intl.ListFormat('en-us', { type: 'conjunction' }).format(SHADOW_REQUIRED_PROPERTIES)}.`,
|
|
16
16
|
[ERROR_INVALID_PROP]: 'Unknown property {{ key }}.',
|
|
17
17
|
},
|
|
18
18
|
docs: {
|
|
@@ -20,9 +20,9 @@ const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
|
20
20
|
const rule: LintRule<typeof ERROR_STR | typeof ERROR_OBJ | typeof ERROR_LINE_CAP | typeof ERROR_INVALID_PROP> = {
|
|
21
21
|
meta: {
|
|
22
22
|
messages: {
|
|
23
|
-
[ERROR_STR]: `Value most be one of ${new Intl.ListFormat(
|
|
24
|
-
[ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat(
|
|
25
|
-
[ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat(
|
|
23
|
+
[ERROR_STR]: `Value most be one of ${new Intl.ListFormat('en-us', { type: 'disjunction' }).format(STROKE_STYLE_STRING_VALUES)}.`,
|
|
24
|
+
[ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat('en-us', { type: 'conjunction' }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
|
|
25
|
+
[ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat('en-us', { type: 'disjunction' }).format(STROKE_STYLE_LINE_CAP_VALUES)}.`,
|
|
26
26
|
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
27
27
|
},
|
|
28
28
|
docs: {
|
|
@@ -12,7 +12,7 @@ const ERROR_INVALID_PROP = 'ERROR_INVALID_PROP';
|
|
|
12
12
|
const rule: LintRule<typeof ERROR | typeof ERROR_INVALID_PROP> = {
|
|
13
13
|
meta: {
|
|
14
14
|
messages: {
|
|
15
|
-
[ERROR]: `Missing required properties: ${new Intl.ListFormat(
|
|
15
|
+
[ERROR]: `Missing required properties: ${new Intl.ListFormat('en-us', { type: 'conjunction' }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
|
|
16
16
|
[ERROR_INVALID_PROP]: 'Unknown property: {{ key }}.',
|
|
17
17
|
},
|
|
18
18
|
docs: {
|
package/src/parse/load.ts
CHANGED
|
@@ -2,10 +2,10 @@ import * as momoa from '@humanwhocodes/momoa';
|
|
|
2
2
|
import {
|
|
3
3
|
type BundleOptions,
|
|
4
4
|
bundle,
|
|
5
|
+
encodeFragment,
|
|
5
6
|
getObjMember,
|
|
6
7
|
type InputSource,
|
|
7
8
|
type InputSourceWithDocument,
|
|
8
|
-
type RefMap,
|
|
9
9
|
replaceNode,
|
|
10
10
|
traverse,
|
|
11
11
|
} from '@terrazzo/json-schema-tools';
|
|
@@ -75,9 +75,6 @@ export async function loadSources(
|
|
|
75
75
|
}));
|
|
76
76
|
/** The sources array, indexed by filename */
|
|
77
77
|
let sourceByFilename: Record<string, InputSourceWithDocument> = {};
|
|
78
|
-
/** Mapping of all final $ref resolutions. This will be used to generate the graph later. */
|
|
79
|
-
let refMap: RefMap = {};
|
|
80
|
-
|
|
81
78
|
try {
|
|
82
79
|
const result = await bundle(sources, {
|
|
83
80
|
req,
|
|
@@ -86,7 +83,6 @@ export async function loadSources(
|
|
|
86
83
|
});
|
|
87
84
|
document = result.document;
|
|
88
85
|
sourceByFilename = result.sources;
|
|
89
|
-
refMap = result.refMap;
|
|
90
86
|
for (const [filename, source] of Object.entries(result.sources)) {
|
|
91
87
|
const i = sources.findIndex((s) => s.filename.href === filename);
|
|
92
88
|
if (i === -1) {
|
|
@@ -111,9 +107,14 @@ export async function loadSources(
|
|
|
111
107
|
}
|
|
112
108
|
logger.debug({ ...entry, message: `JSON loaded`, timing: performance.now() - firstLoad });
|
|
113
109
|
|
|
114
|
-
const rootSource = {
|
|
110
|
+
const rootSource = {
|
|
111
|
+
filename: sources[0]!.filename!,
|
|
112
|
+
document,
|
|
113
|
+
src: momoa.print(document, { indent: 2 }).replace(/\\\//g, '/'),
|
|
114
|
+
};
|
|
115
|
+
|
|
115
116
|
return {
|
|
116
|
-
tokens: processTokens(rootSource, { config, logger,
|
|
117
|
+
tokens: processTokens(rootSource, { config, logger, sources, sourceByFilename }),
|
|
117
118
|
sources,
|
|
118
119
|
};
|
|
119
120
|
}
|
|
@@ -141,7 +142,7 @@ function transformer(transform: TransformVisitors): BundleOptions['parse'] {
|
|
|
141
142
|
const ctx = { filename, parent, path };
|
|
142
143
|
const next$type = getObjMember(node, '$type');
|
|
143
144
|
if (next$type?.type === 'String') {
|
|
144
|
-
const jsonPath =
|
|
145
|
+
const jsonPath = encodeFragment(path);
|
|
145
146
|
if (jsonPath.startsWith(lastPath)) {
|
|
146
147
|
last$type = next$type.value;
|
|
147
148
|
}
|
package/src/parse/process.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import {
|
|
3
|
+
encodeFragment,
|
|
4
|
+
findNode,
|
|
5
|
+
getObjMember,
|
|
6
|
+
type InputSourceWithDocument,
|
|
7
|
+
mergeObjects,
|
|
8
|
+
parseRef,
|
|
9
|
+
replaceNode,
|
|
10
|
+
traverse,
|
|
11
|
+
} from '@terrazzo/json-schema-tools';
|
|
12
|
+
import { type GroupNormalized, isAlias, type TokenNormalizedSet } from '@terrazzo/token-tools';
|
|
3
13
|
import { filterResolverPaths } from '../lib/resolver-utils.js';
|
|
4
14
|
import type Logger from '../logger.js';
|
|
5
15
|
import { isLikelyResolver } from '../resolver/validate.js';
|
|
6
|
-
import type { ConfigInit } from '../types.js';
|
|
16
|
+
import type { ConfigInit, RefMap } from '../types.js';
|
|
7
17
|
import { normalize } from './normalize.js';
|
|
8
18
|
import {
|
|
19
|
+
aliasToGroupRef,
|
|
9
20
|
graphAliases,
|
|
10
21
|
groupFromNode,
|
|
11
22
|
refToTokenID,
|
|
@@ -18,17 +29,136 @@ export interface ProcessTokensOptions {
|
|
|
18
29
|
config: ConfigInit;
|
|
19
30
|
logger: Logger;
|
|
20
31
|
sourceByFilename: Record<string, InputSourceWithDocument>;
|
|
21
|
-
refMap: RefMap;
|
|
22
32
|
sources: InputSourceWithDocument[];
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function processTokens(
|
|
26
36
|
rootSource: InputSourceWithDocument,
|
|
27
|
-
{ config, logger, sourceByFilename
|
|
37
|
+
{ config, logger, sourceByFilename }: ProcessTokensOptions,
|
|
28
38
|
): TokenNormalizedSet {
|
|
29
39
|
const entry = { group: 'parser' as const, label: 'init' };
|
|
30
40
|
|
|
31
|
-
//
|
|
41
|
+
// 1. Inline $refs to discover any additional tokens
|
|
42
|
+
const refMap: RefMap = {};
|
|
43
|
+
function resolveRef(node: momoa.StringNode, chain: string[]): momoa.AnyNode {
|
|
44
|
+
const { subpath } = parseRef(node.value);
|
|
45
|
+
if (!subpath) {
|
|
46
|
+
logger.error({ ...entry, message: 'Can’t resolve $ref', node, src: rootSource.src });
|
|
47
|
+
// exit
|
|
48
|
+
}
|
|
49
|
+
const next = findNode(rootSource.document, subpath);
|
|
50
|
+
if (next?.type === 'Object') {
|
|
51
|
+
const next$ref = getObjMember(next, '$ref');
|
|
52
|
+
if (next$ref && next$ref.type === 'String') {
|
|
53
|
+
if (chain.includes(next$ref.value)) {
|
|
54
|
+
logger.error({
|
|
55
|
+
...entry,
|
|
56
|
+
message: `Circular $ref detected: ${JSON.stringify(next$ref.value)}`,
|
|
57
|
+
node: next$ref,
|
|
58
|
+
src: rootSource.src,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
chain.push(next$ref.value);
|
|
62
|
+
return resolveRef(next$ref, chain);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
const inlineStart = performance.now();
|
|
68
|
+
traverse(rootSource.document, {
|
|
69
|
+
enter(node, _parent, rawPath) {
|
|
70
|
+
if (rawPath.includes('$extensions') || node.type !== 'Object') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const $ref = node.type === 'Object' ? (getObjMember(node, '$ref') as momoa.StringNode) : undefined;
|
|
74
|
+
if (!$ref) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if ($ref.type !== 'String') {
|
|
78
|
+
logger.error({ ...entry, message: 'Invalid $ref. Expected string.', node: $ref, src: rootSource.src });
|
|
79
|
+
}
|
|
80
|
+
const jsonID = encodeFragment(rawPath);
|
|
81
|
+
refMap[jsonID] = { filename: rootSource.filename.href, refChain: [$ref.value] };
|
|
82
|
+
const resolved = resolveRef($ref, refMap[jsonID]!.refChain);
|
|
83
|
+
if (resolved.type === 'Object') {
|
|
84
|
+
node.members.splice(
|
|
85
|
+
node.members.findIndex((m) => m.name.type === 'String' && m.name.value === '$ref'),
|
|
86
|
+
1,
|
|
87
|
+
);
|
|
88
|
+
replaceNode(node, mergeObjects(resolved, node));
|
|
89
|
+
} else {
|
|
90
|
+
replaceNode(node, resolved);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
logger.debug({ ...entry, message: 'Inline aliases', timing: performance.now() - inlineStart });
|
|
95
|
+
|
|
96
|
+
// 2. Resolve $extends to discover any more additional tokens
|
|
97
|
+
function flatten$extends(node: momoa.ObjectNode, chain: string[]) {
|
|
98
|
+
const memberKeys = node.members.map((m) => m.name.type === 'String' && m.name.value).filter(Boolean) as string[];
|
|
99
|
+
|
|
100
|
+
let extended: momoa.ObjectNode | undefined;
|
|
101
|
+
|
|
102
|
+
if (memberKeys.includes('$extends')) {
|
|
103
|
+
const $extends = getObjMember(node, '$extends') as momoa.StringNode;
|
|
104
|
+
if ($extends.type !== 'String') {
|
|
105
|
+
logger.error({ ...entry, message: '$extends must be a string', node: $extends, src: rootSource.src });
|
|
106
|
+
}
|
|
107
|
+
if (memberKeys.includes('$value')) {
|
|
108
|
+
logger.error({ ...entry, message: '$extends can’t exist within a token', node: $extends, src: rootSource.src });
|
|
109
|
+
}
|
|
110
|
+
const next = isAlias($extends.value) ? aliasToGroupRef($extends.value) : undefined;
|
|
111
|
+
if (!next) {
|
|
112
|
+
logger.error({ ...entry, message: '$extends must be a valid alias', node: $extends, src: rootSource.src });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
chain.includes(next!.$ref) ||
|
|
117
|
+
// Check that $extends is not importing from higher up (could go in either direction, which is why we check both ways)
|
|
118
|
+
chain.some((value) => value.startsWith(next!.$ref) || next!.$ref.startsWith(value))
|
|
119
|
+
) {
|
|
120
|
+
logger.error({ ...entry, message: 'Circular $extends detected', node: $extends, src: rootSource.src });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
chain.push(next!.$ref);
|
|
124
|
+
extended = findNode(rootSource.document, parseRef(next!.$ref).subpath ?? []);
|
|
125
|
+
if (!extended) {
|
|
126
|
+
logger.error({ ...entry, message: 'Could not resolve $extends', node: $extends, src: rootSource.src });
|
|
127
|
+
}
|
|
128
|
+
if (extended!.type !== 'Object') {
|
|
129
|
+
logger.error({ ...entry, message: '$extends must resolve to a group of tokens', node });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// To ensure this is resolvable, try and flatten this node first (will catch circular refs)
|
|
133
|
+
flatten$extends(extended!, chain);
|
|
134
|
+
|
|
135
|
+
replaceNode(node, mergeObjects(extended!, node));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Deeply-traverse for any interior $extends (even if it wasn’t at the top level)
|
|
139
|
+
for (const member of node.members) {
|
|
140
|
+
if (
|
|
141
|
+
member.value.type === 'Object' &&
|
|
142
|
+
member.name.type === 'String' &&
|
|
143
|
+
!['$value', '$extensions'].includes(member.name.value)
|
|
144
|
+
) {
|
|
145
|
+
traverse(member.value, {
|
|
146
|
+
enter(subnode, _parent) {
|
|
147
|
+
if (subnode.type === 'Object') {
|
|
148
|
+
flatten$extends(subnode, chain);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const extendsStart = performance.now();
|
|
157
|
+
const extendsChain: string[] = [];
|
|
158
|
+
flatten$extends(rootSource.document.body as momoa.ObjectNode, extendsChain);
|
|
159
|
+
logger.debug({ ...entry, message: 'Resolving $extends', timing: performance.now() - extendsStart });
|
|
160
|
+
|
|
161
|
+
// 3. Parse discovered tokens
|
|
32
162
|
const firstPass = performance.now();
|
|
33
163
|
const tokens: TokenNormalizedSet = {};
|
|
34
164
|
// micro-optimization: while we’re iterating over tokens, keeping a “hot”
|
|
@@ -37,7 +167,7 @@ export function processTokens(
|
|
|
37
167
|
const tokenIDs: string[] = [];
|
|
38
168
|
const groups: Record<string, GroupNormalized> = {};
|
|
39
169
|
|
|
40
|
-
//
|
|
170
|
+
// 3a. Token & group population
|
|
41
171
|
const isResolver = isLikelyResolver(rootSource.document);
|
|
42
172
|
traverse(rootSource.document, {
|
|
43
173
|
enter(node, _parent, rawPath) {
|
|
@@ -61,7 +191,7 @@ export function processTokens(
|
|
|
61
191
|
logger.debug({ ...entry, message: 'Parsing: 1st pass', timing: performance.now() - firstPass });
|
|
62
192
|
const secondPass = performance.now();
|
|
63
193
|
|
|
64
|
-
//
|
|
194
|
+
// 3b. Resolve originalValue and original sources
|
|
65
195
|
for (const source of Object.values(sourceByFilename)) {
|
|
66
196
|
traverse(source.document, {
|
|
67
197
|
enter(node, _parent, path) {
|
|
@@ -82,18 +212,18 @@ export function processTokens(
|
|
|
82
212
|
});
|
|
83
213
|
}
|
|
84
214
|
|
|
85
|
-
//
|
|
215
|
+
// 3c. DTCG alias resolution
|
|
86
216
|
// Unlike $refs which can be resolved as we go, these can’t happen until the final, flattened set
|
|
87
217
|
resolveAliases(tokens, { logger, sources: sourceByFilename, refMap });
|
|
88
218
|
logger.debug({ ...entry, message: 'Parsing: 2nd pass', timing: performance.now() - secondPass });
|
|
89
219
|
|
|
90
|
-
//
|
|
220
|
+
// 4. Alias graph
|
|
91
221
|
// We’ve resolved aliases, but we need this pass for reverse linking i.e. “aliasedBy”
|
|
92
222
|
const aliasStart = performance.now();
|
|
93
223
|
graphAliases(refMap, { tokens, logger, sources: sourceByFilename });
|
|
94
224
|
logger.debug({ ...entry, message: 'Alias graph built', timing: performance.now() - aliasStart });
|
|
95
225
|
|
|
96
|
-
//
|
|
226
|
+
// 5. normalize
|
|
97
227
|
// Allow for some minor variance in inputs, and be nice to folks.
|
|
98
228
|
const normalizeStart = performance.now();
|
|
99
229
|
for (const id of tokenIDs) {
|
|
@@ -102,15 +232,12 @@ export function processTokens(
|
|
|
102
232
|
}
|
|
103
233
|
logger.debug({ ...entry, message: 'Normalized values', timing: performance.now() - normalizeStart });
|
|
104
234
|
|
|
105
|
-
//
|
|
235
|
+
// 6. alphabetize & filter
|
|
106
236
|
// This can’t happen until the last step, where we’re 100% sure we’ve resolved everything.
|
|
237
|
+
const sortStart = performance.now();
|
|
107
238
|
const tokensSorted: TokenNormalizedSet = {};
|
|
108
239
|
tokenIDs.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
109
240
|
for (const path of tokenIDs) {
|
|
110
|
-
// Filter out any tokens in $defs (we needed to reference them earlier, but shouldn’t include them in the final assortment)
|
|
111
|
-
if (path.includes('/$defs/')) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
241
|
const id = refToTokenID(path)!;
|
|
115
242
|
tokensSorted[id] = tokens[path]!;
|
|
116
243
|
}
|
|
@@ -118,6 +245,7 @@ export function processTokens(
|
|
|
118
245
|
for (const group of Object.values(groups)) {
|
|
119
246
|
group.tokens.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
120
247
|
}
|
|
248
|
+
logger.debug({ ...entry, message: 'Sorted tokens', timing: performance.now() - sortStart });
|
|
121
249
|
|
|
122
250
|
return tokensSorted;
|
|
123
251
|
}
|
package/src/parse/token.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
-
import { getObjMember, type InputSourceWithDocument, parseRef
|
|
2
|
+
import { encodeFragment, getObjMember, type InputSourceWithDocument, parseRef } from '@terrazzo/json-schema-tools';
|
|
3
3
|
import {
|
|
4
4
|
type GroupNormalized,
|
|
5
5
|
isAlias,
|
|
@@ -9,10 +9,20 @@ import {
|
|
|
9
9
|
} from '@terrazzo/token-tools';
|
|
10
10
|
import wcmatch from 'wildcard-match';
|
|
11
11
|
import type { default as Logger } from '../logger.js';
|
|
12
|
-
import type { Config, ReferenceObject } from '../types.js';
|
|
12
|
+
import type { Config, ReferenceObject, RefMap } from '../types.js';
|
|
13
13
|
|
|
14
14
|
/** Convert valid DTCG alias to $ref */
|
|
15
|
-
export function
|
|
15
|
+
export function aliasToGroupRef(alias: string): ReferenceObject | undefined {
|
|
16
|
+
const id = parseAlias(alias);
|
|
17
|
+
// if this is invalid, stop
|
|
18
|
+
if (id === alias) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return { $ref: `#/${id.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\./g, '/')}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Convert valid DTCG alias to $ref */
|
|
25
|
+
export function aliasToTokenRef(alias: string, mode?: string): ReferenceObject | undefined {
|
|
16
26
|
const id = parseAlias(alias);
|
|
17
27
|
// if this is invalid, stop
|
|
18
28
|
if (id === alias) {
|
|
@@ -40,12 +50,12 @@ export function tokenFromNode(
|
|
|
40
50
|
return undefined;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
const jsonID =
|
|
53
|
+
const jsonID = encodeFragment(path);
|
|
44
54
|
const id = path.join('.');
|
|
45
55
|
|
|
46
56
|
const originalToken = momoa.evaluate(node) as any;
|
|
47
57
|
|
|
48
|
-
const groupID =
|
|
58
|
+
const groupID = encodeFragment(path.slice(0, -1));
|
|
49
59
|
const group = groups[groupID]!;
|
|
50
60
|
if (group?.tokens && !group.tokens.includes(id)) {
|
|
51
61
|
group.tokens.push(id);
|
|
@@ -132,7 +142,7 @@ export function tokenRawValuesFromNode(
|
|
|
132
142
|
return undefined;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
const jsonID =
|
|
145
|
+
const jsonID = encodeFragment(path);
|
|
136
146
|
const rawValues: TokenRawValues = {
|
|
137
147
|
jsonID,
|
|
138
148
|
originalValue: momoa.evaluate(node),
|
|
@@ -173,7 +183,7 @@ export function groupFromNode(
|
|
|
173
183
|
{ path, groups }: { path: string[]; groups: Record<string, GroupNormalized> },
|
|
174
184
|
): GroupNormalized {
|
|
175
185
|
const id = path.join('.');
|
|
176
|
-
const jsonID =
|
|
186
|
+
const jsonID = encodeFragment(path);
|
|
177
187
|
|
|
178
188
|
// group
|
|
179
189
|
if (!groups[jsonID]) {
|
|
@@ -354,7 +364,7 @@ export function aliasToMomoa(
|
|
|
354
364
|
end: { line: -1, column: -1, offset: 0 },
|
|
355
365
|
},
|
|
356
366
|
): momoa.ObjectNode | undefined {
|
|
357
|
-
const $ref =
|
|
367
|
+
const $ref = aliasToTokenRef(alias);
|
|
358
368
|
if (!$ref) {
|
|
359
369
|
return;
|
|
360
370
|
}
|
|
@@ -383,6 +393,10 @@ export function refToTokenID($ref: ReferenceObject | string): string | undefined
|
|
|
383
393
|
return;
|
|
384
394
|
}
|
|
385
395
|
const { subpath } = parseRef(path);
|
|
396
|
+
// if this ID comes from #/$defs/…, strip the first 2 segments to get the global ID
|
|
397
|
+
if (subpath?.[0] === '$defs') {
|
|
398
|
+
subpath.splice(0, 2);
|
|
399
|
+
}
|
|
386
400
|
return (subpath?.length && subpath.join('.').replace(/\.(\$value|\$extensions).*$/, '')) || undefined;
|
|
387
401
|
}
|
|
388
402
|
|
|
@@ -422,7 +436,7 @@ const EXPECTED_NESTED_ALIAS: Record<string, Record<string, string[]>> = {
|
|
|
422
436
|
};
|
|
423
437
|
|
|
424
438
|
/**
|
|
425
|
-
* Resolve DTCG aliases
|
|
439
|
+
* Resolve DTCG aliases, $extends, and $ref
|
|
426
440
|
*/
|
|
427
441
|
export function resolveAliases(
|
|
428
442
|
tokens: TokenNormalizedSet,
|
|
@@ -438,7 +452,7 @@ export function resolveAliases(
|
|
|
438
452
|
|
|
439
453
|
for (const mode of Object.keys(token.mode)) {
|
|
440
454
|
function resolveInner(alias: string, refChain: string[]): string {
|
|
441
|
-
const nextRef =
|
|
455
|
+
const nextRef = aliasToTokenRef(alias, mode)?.$ref;
|
|
442
456
|
if (!nextRef) {
|
|
443
457
|
logger.error({ ...aliasEntry, message: `Internal error resolving ${JSON.stringify(refChain)}` });
|
|
444
458
|
throw new Error('Internal error');
|