@terrazzo/parser 0.10.4 → 2.0.0-alpha.1
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 +67 -0
- package/dist/index.d.ts +112 -325
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2194 -3621
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/build/index.ts +42 -42
- package/src/config.ts +13 -6
- package/src/lib/code-frame.ts +3 -0
- package/src/lib/momoa.ts +10 -0
- package/src/lint/index.ts +41 -37
- package/src/lint/plugin-core/index.ts +73 -16
- package/src/lint/plugin-core/rules/colorspace.ts +4 -0
- package/src/lint/plugin-core/rules/duplicate-values.ts +2 -0
- package/src/lint/plugin-core/rules/max-gamut.ts +24 -4
- package/src/lint/plugin-core/rules/no-type-on-alias.ts +29 -0
- package/src/lint/plugin-core/rules/required-modes.ts +2 -0
- package/src/lint/plugin-core/rules/required-typography-properties.ts +13 -3
- package/src/lint/plugin-core/rules/valid-boolean.ts +41 -0
- package/src/lint/plugin-core/rules/valid-border.ts +57 -0
- package/src/lint/plugin-core/rules/valid-color.ts +265 -0
- package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +83 -0
- package/src/lint/plugin-core/rules/valid-dimension.ts +199 -0
- package/src/lint/plugin-core/rules/valid-duration.ts +123 -0
- package/src/lint/plugin-core/rules/valid-font-family.ts +68 -0
- package/src/lint/plugin-core/rules/valid-font-weight.ts +89 -0
- package/src/lint/plugin-core/rules/valid-gradient.ts +79 -0
- package/src/lint/plugin-core/rules/valid-link.ts +41 -0
- package/src/lint/plugin-core/rules/valid-number.ts +63 -0
- package/src/lint/plugin-core/rules/valid-shadow.ts +67 -0
- package/src/lint/plugin-core/rules/valid-string.ts +41 -0
- package/src/lint/plugin-core/rules/valid-stroke-style.ts +104 -0
- package/src/lint/plugin-core/rules/valid-transition.ts +61 -0
- package/src/lint/plugin-core/rules/valid-typography.ts +67 -0
- package/src/logger.ts +70 -59
- package/src/parse/index.ts +23 -328
- package/src/parse/load.ts +257 -0
- package/src/parse/normalize.ts +134 -170
- package/src/parse/token.ts +530 -0
- package/src/types.ts +106 -28
- package/src/parse/alias.ts +0 -369
- package/src/parse/json.ts +0 -211
- package/src/parse/validate.ts +0 -961
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import * as momoa from '@humanwhocodes/momoa';
|
|
2
|
+
import { getObjMember, parseRef, type RefMap } from '@terrazzo/json-schema-tools';
|
|
3
|
+
import {
|
|
4
|
+
type GroupNormalized,
|
|
5
|
+
isAlias,
|
|
6
|
+
parseAlias,
|
|
7
|
+
type TokenNormalized,
|
|
8
|
+
type TokenNormalizedSet,
|
|
9
|
+
} from '@terrazzo/token-tools';
|
|
10
|
+
import wcmatch from 'wildcard-match';
|
|
11
|
+
import type { default as Logger } from '../logger.js';
|
|
12
|
+
import type { Config, InputSource, ReferenceObject } from '../types.js';
|
|
13
|
+
|
|
14
|
+
/** Convert valid DTCG alias to $ref */
|
|
15
|
+
export function aliasToRef(alias: string, mode?: string): ReferenceObject | undefined {
|
|
16
|
+
const id = parseAlias(alias);
|
|
17
|
+
// if this is invalid, stop
|
|
18
|
+
if (id === alias) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
$ref: `#/${id.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\./g, '/')}${mode && mode !== '.' ? `/$extensions/mode/${mode}` : ''}/$value`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TokenFromNodeOptions {
|
|
27
|
+
groups: Record<string, GroupNormalized>;
|
|
28
|
+
path: string[];
|
|
29
|
+
source: InputSource;
|
|
30
|
+
ignore: Config['ignore'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Generate a TokenNormalized from a Momoa node */
|
|
34
|
+
export function tokenFromNode(
|
|
35
|
+
node: momoa.AnyNode,
|
|
36
|
+
{ groups, path, source, ignore }: TokenFromNodeOptions,
|
|
37
|
+
): TokenNormalized | undefined {
|
|
38
|
+
const isToken = node.type === 'Object' && getObjMember(node, '$value') && !path.includes('$extensions');
|
|
39
|
+
if (!isToken) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const jsonID = `#/${path.join('/')}`;
|
|
44
|
+
const id = path.join('.');
|
|
45
|
+
|
|
46
|
+
const originalToken = momoa.evaluate(node) as any;
|
|
47
|
+
|
|
48
|
+
const groupID = `#/${path.slice(0, -1).join('/')}`;
|
|
49
|
+
const group = groups[groupID]!;
|
|
50
|
+
if (group?.tokens && !group.tokens.includes(id)) {
|
|
51
|
+
group.tokens.push(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nodeSource = { filename: source.filename?.href, node };
|
|
55
|
+
const token: TokenNormalized = {
|
|
56
|
+
id,
|
|
57
|
+
$type: originalToken.$type || group.$type,
|
|
58
|
+
$description: originalToken.$description || undefined,
|
|
59
|
+
$deprecated: originalToken.$deprecated ?? group.$deprecated ?? undefined, // ⚠️ MUST use ?? here to inherit false correctly
|
|
60
|
+
$value: originalToken.$value,
|
|
61
|
+
$extensions: originalToken.$extensions || undefined,
|
|
62
|
+
aliasChain: undefined,
|
|
63
|
+
aliasedBy: undefined,
|
|
64
|
+
aliasOf: undefined,
|
|
65
|
+
partialAliasOf: undefined,
|
|
66
|
+
dependencies: undefined,
|
|
67
|
+
group,
|
|
68
|
+
originalValue: undefined, // undefined because we are not sure if the value has been modified or not
|
|
69
|
+
source: nodeSource,
|
|
70
|
+
jsonID,
|
|
71
|
+
mode: {
|
|
72
|
+
'.': {
|
|
73
|
+
$value: originalToken.$value,
|
|
74
|
+
aliasOf: undefined,
|
|
75
|
+
aliasChain: undefined,
|
|
76
|
+
partialAliasOf: undefined,
|
|
77
|
+
aliasedBy: undefined,
|
|
78
|
+
originalValue: undefined,
|
|
79
|
+
dependencies: undefined,
|
|
80
|
+
source: {
|
|
81
|
+
...nodeSource,
|
|
82
|
+
node: (getObjMember(nodeSource.node, '$value') ?? nodeSource.node) as momoa.ObjectNode,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// after assembling token, handle ignores to see if the final result should be ignored or not
|
|
89
|
+
// filter out ignored
|
|
90
|
+
if ((ignore?.deprecated && token.$deprecated) || (ignore?.tokens && wcmatch(ignore.tokens)(token.id))) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const $extensions = getObjMember(node, '$extensions');
|
|
95
|
+
if ($extensions) {
|
|
96
|
+
const modeNode = getObjMember($extensions as momoa.ObjectNode, 'mode') as momoa.ObjectNode;
|
|
97
|
+
for (const mode of Object.keys((token.$extensions as any).mode)) {
|
|
98
|
+
const modeValue = (token.$extensions as any).mode[mode];
|
|
99
|
+
token.mode[mode] = {
|
|
100
|
+
$value: modeValue,
|
|
101
|
+
aliasOf: undefined,
|
|
102
|
+
aliasChain: undefined,
|
|
103
|
+
partialAliasOf: undefined,
|
|
104
|
+
aliasedBy: undefined,
|
|
105
|
+
originalValue: undefined,
|
|
106
|
+
dependencies: undefined,
|
|
107
|
+
source: {
|
|
108
|
+
...nodeSource,
|
|
109
|
+
node: getObjMember(modeNode, mode) as any,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TokenRawValues {
|
|
118
|
+
jsonID: string;
|
|
119
|
+
originalValue: any;
|
|
120
|
+
source: TokenNormalized['source'];
|
|
121
|
+
mode: Record<string, { originalValue: any; source: TokenNormalized['source'] }>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Generate originalValue and source from node */
|
|
125
|
+
export function tokenRawValuesFromNode(
|
|
126
|
+
node: momoa.AnyNode,
|
|
127
|
+
{ filename, path }: { filename: string; path: string[] },
|
|
128
|
+
): TokenRawValues | undefined {
|
|
129
|
+
const isToken = node.type === 'Object' && getObjMember(node, '$value') && !path.includes('$extensions');
|
|
130
|
+
if (!isToken) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const jsonID = `#/${path.join('/')}`;
|
|
135
|
+
const rawValues: TokenRawValues = {
|
|
136
|
+
jsonID,
|
|
137
|
+
originalValue: momoa.evaluate(node),
|
|
138
|
+
source: { loc: filename, filename, node: node as momoa.ObjectNode },
|
|
139
|
+
mode: {},
|
|
140
|
+
};
|
|
141
|
+
rawValues.mode['.'] = {
|
|
142
|
+
originalValue: rawValues.originalValue.$value,
|
|
143
|
+
source: { ...rawValues.source, node: getObjMember(node as momoa.ObjectNode, '$value') as momoa.ObjectNode },
|
|
144
|
+
};
|
|
145
|
+
const $extensions = getObjMember(node, '$extensions');
|
|
146
|
+
if ($extensions) {
|
|
147
|
+
const modes = getObjMember($extensions as momoa.ObjectNode, 'mode');
|
|
148
|
+
if (modes) {
|
|
149
|
+
for (const modeMember of (modes as momoa.ObjectNode).members) {
|
|
150
|
+
const mode = (modeMember.name as momoa.StringNode).value;
|
|
151
|
+
rawValues.mode[mode] = {
|
|
152
|
+
originalValue: momoa.evaluate(modeMember.value),
|
|
153
|
+
source: { loc: filename, filename, node: modeMember.value as momoa.ObjectNode },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return rawValues;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Arbitrary keys that should be associated with a token group */
|
|
163
|
+
const GROUP_PROPERTIES = ['$deprecated', '$description', '$extensions', '$type'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate a group from a node.
|
|
167
|
+
* This method mutates the groups index as it goes because of group inheritance.
|
|
168
|
+
* As it encounters new groups it may have to update other groups.
|
|
169
|
+
*/
|
|
170
|
+
export function groupFromNode(
|
|
171
|
+
node: momoa.ObjectNode,
|
|
172
|
+
{ path, groups }: { path: string[]; groups: Record<string, GroupNormalized> },
|
|
173
|
+
): GroupNormalized {
|
|
174
|
+
const id = path.join('.');
|
|
175
|
+
const jsonID = `#/${path.join('/')}`;
|
|
176
|
+
|
|
177
|
+
// group
|
|
178
|
+
if (!groups[jsonID]) {
|
|
179
|
+
groups[jsonID] = {
|
|
180
|
+
id,
|
|
181
|
+
$deprecated: undefined,
|
|
182
|
+
$description: undefined,
|
|
183
|
+
$extensions: undefined,
|
|
184
|
+
$type: undefined,
|
|
185
|
+
tokens: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// first, copy all parent groups’ properties into local, since they cascade
|
|
190
|
+
const groupIDs = Object.keys(groups);
|
|
191
|
+
groupIDs.sort(); // these may not be sorted; re-sort just in case (order determines final values)
|
|
192
|
+
for (const groupID of groupIDs) {
|
|
193
|
+
const isParentGroup = jsonID.startsWith(groupID) && groupID !== jsonID;
|
|
194
|
+
if (isParentGroup) {
|
|
195
|
+
groups[jsonID].$deprecated = groups[groupID]?.$deprecated ?? groups[jsonID].$deprecated;
|
|
196
|
+
groups[jsonID].$description = groups[groupID]?.$description ?? groups[jsonID].$description;
|
|
197
|
+
groups[jsonID].$type = groups[groupID]?.$type ?? groups[jsonID].$type;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// next, override cascading values with local
|
|
202
|
+
for (const m of node.members) {
|
|
203
|
+
if (m.name.type !== 'String' || !GROUP_PROPERTIES.includes(m.name.value)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
(groups as any)[jsonID]![m.name.value] = momoa.evaluate(m.value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return groups[jsonID]!;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface GraphAliasesOptions {
|
|
213
|
+
tokens: TokenNormalizedSet;
|
|
214
|
+
sources: Record<string, InputSource>;
|
|
215
|
+
logger: Logger;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Link and reverse-link tokens in one pass.
|
|
220
|
+
*/
|
|
221
|
+
export function graphAliases(refMap: RefMap, { tokens, logger, sources }: GraphAliasesOptions) {
|
|
222
|
+
// mini-helper that probably shouldn’t be used outside this function
|
|
223
|
+
const getTokenRef = (ref: string) => ref.replace(/\/(\$value|\$extensions)\/?.*/, '');
|
|
224
|
+
|
|
225
|
+
for (const [jsonID, { refChain }] of Object.entries(refMap)) {
|
|
226
|
+
if (!refChain.length) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const mode = jsonID.match(/\/\$extensions\/mode\/([^/]+)/)?.[1] || '.';
|
|
231
|
+
const rootRef = getTokenRef(jsonID);
|
|
232
|
+
const modeValue = tokens[rootRef]?.mode[mode];
|
|
233
|
+
if (!modeValue) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// aliasChain + dependencies
|
|
238
|
+
if (!modeValue.dependencies) {
|
|
239
|
+
modeValue.dependencies = [];
|
|
240
|
+
}
|
|
241
|
+
modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies!.includes(r)));
|
|
242
|
+
modeValue.dependencies.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
243
|
+
|
|
244
|
+
// Top alias
|
|
245
|
+
const isTopLevelAlias = jsonID.endsWith('/$value') || tokens[jsonID];
|
|
246
|
+
if (isTopLevelAlias) {
|
|
247
|
+
modeValue.aliasOf = refToTokenID(refChain.at(-1)!);
|
|
248
|
+
const aliasChain = refChain.map(refToTokenID) as string[];
|
|
249
|
+
modeValue.aliasChain = [...aliasChain];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Partial alias
|
|
253
|
+
const partial = jsonID
|
|
254
|
+
.replace(/.*\/\$value\/?/, '')
|
|
255
|
+
.split('/')
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
if (partial.length && modeValue.$value && typeof modeValue.$value === 'object') {
|
|
258
|
+
let node: any = modeValue.$value;
|
|
259
|
+
let sourceNode = modeValue.source.node as momoa.AnyNode;
|
|
260
|
+
if (!modeValue.partialAliasOf) {
|
|
261
|
+
modeValue.partialAliasOf = Array.isArray(modeValue.$value) || tokens[rootRef]?.$type === 'shadow' ? [] : {};
|
|
262
|
+
}
|
|
263
|
+
let partialAliasOf = modeValue.partialAliasOf as any;
|
|
264
|
+
// special case: for shadows, normalize object to array
|
|
265
|
+
if (tokens[rootRef]?.$type === 'shadow' && !Array.isArray(node)) {
|
|
266
|
+
if (Array.isArray(modeValue.partialAliasOf) && !modeValue.partialAliasOf.length) {
|
|
267
|
+
modeValue.partialAliasOf.push({} as any);
|
|
268
|
+
}
|
|
269
|
+
partialAliasOf = (modeValue.partialAliasOf as any)[0]!;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < partial.length; i++) {
|
|
273
|
+
let key = partial[i] as string | number;
|
|
274
|
+
if (String(Number(key)) === key) {
|
|
275
|
+
key = Number(key);
|
|
276
|
+
}
|
|
277
|
+
if (key in node && typeof node[key] !== 'undefined') {
|
|
278
|
+
node = node[key];
|
|
279
|
+
if (sourceNode.type === 'Object') {
|
|
280
|
+
sourceNode = getObjMember(sourceNode, key as string) ?? sourceNode;
|
|
281
|
+
} else if (sourceNode.type === 'Array') {
|
|
282
|
+
sourceNode = sourceNode.elements[key as number]?.value ?? sourceNode;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// last node: apply partial alias
|
|
286
|
+
if (i === partial.length - 1) {
|
|
287
|
+
const aliasedID = getTokenRef(refChain.at(-1)!);
|
|
288
|
+
if (!(aliasedID in tokens)) {
|
|
289
|
+
logger.error({
|
|
290
|
+
group: 'parser',
|
|
291
|
+
label: 'init',
|
|
292
|
+
message: `Invalid alias: ${aliasedID}`,
|
|
293
|
+
node: sourceNode,
|
|
294
|
+
src: sources[tokens[rootRef]!.source.filename!]?.src,
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
partialAliasOf[key] = refToTokenID(aliasedID);
|
|
299
|
+
}
|
|
300
|
+
// otherwise, create deeper structure and continue traversing
|
|
301
|
+
if (!(key in partialAliasOf)) {
|
|
302
|
+
partialAliasOf[key] = Array.isArray(node) ? [] : {};
|
|
303
|
+
}
|
|
304
|
+
partialAliasOf = partialAliasOf[key];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// aliasedBy (reversed)
|
|
309
|
+
const aliasedByRefs = [jsonID, ...refChain].reverse();
|
|
310
|
+
for (let i = 0; i < aliasedByRefs.length; i++) {
|
|
311
|
+
const baseRef = getTokenRef(aliasedByRefs[i]!);
|
|
312
|
+
const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
|
|
313
|
+
if (!baseToken) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const upstream = aliasedByRefs.slice(i + 1);
|
|
317
|
+
if (!upstream.length) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (!baseToken.aliasedBy) {
|
|
321
|
+
baseToken.aliasedBy = [];
|
|
322
|
+
}
|
|
323
|
+
for (let j = 0; j < upstream.length; j++) {
|
|
324
|
+
const downstream = refToTokenID(upstream[j]!)!;
|
|
325
|
+
if (!baseToken.aliasedBy.includes(downstream)) {
|
|
326
|
+
baseToken.aliasedBy.push(downstream);
|
|
327
|
+
if (mode === '.') {
|
|
328
|
+
tokens[baseRef]!.aliasedBy = baseToken.aliasedBy;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true })); // sort, because the ordering is arbitrary and flaky
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (mode === '.') {
|
|
336
|
+
tokens[rootRef]!.aliasChain = modeValue.aliasChain;
|
|
337
|
+
tokens[rootRef]!.aliasedBy = modeValue.aliasedBy;
|
|
338
|
+
tokens[rootRef]!.aliasOf = modeValue.aliasOf;
|
|
339
|
+
tokens[rootRef]!.dependencies = modeValue.dependencies;
|
|
340
|
+
tokens[rootRef]!.partialAliasOf = modeValue.partialAliasOf;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Convert valid DTCG alias to $ref Momoa Node */
|
|
346
|
+
export function aliasToMomoa(
|
|
347
|
+
alias: string,
|
|
348
|
+
loc: momoa.ObjectNode['loc'] = {
|
|
349
|
+
start: { line: -1, column: -1, offset: 0 },
|
|
350
|
+
end: { line: -1, column: -1, offset: 0 },
|
|
351
|
+
},
|
|
352
|
+
): momoa.ObjectNode | undefined {
|
|
353
|
+
const $ref = aliasToRef(alias);
|
|
354
|
+
if (!$ref) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
type: 'Object',
|
|
359
|
+
members: [
|
|
360
|
+
{
|
|
361
|
+
type: 'Member',
|
|
362
|
+
name: { type: 'String', value: '$ref', loc },
|
|
363
|
+
value: { type: 'String', value: $ref.$ref, loc },
|
|
364
|
+
loc,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
loc,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Convert Reference Object to token ID.
|
|
373
|
+
* This can then be turned into an alias by surrounding with { … }
|
|
374
|
+
* ⚠️ This is not mode-aware. This will flatten multiple modes into the same root token.
|
|
375
|
+
*/
|
|
376
|
+
export function refToTokenID($ref: ReferenceObject | string): string | undefined {
|
|
377
|
+
const path = typeof $ref === 'object' ? $ref.$ref : $ref;
|
|
378
|
+
if (typeof path !== 'string') {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const { subpath } = parseRef(path);
|
|
382
|
+
return (subpath?.length && subpath.join('.').replace(/\.(\$value|\$extensions).*$/, '')) || undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const EXPECTED_NESTED_ALIAS: Record<string, Record<string, string[]>> = {
|
|
386
|
+
border: {
|
|
387
|
+
color: ['color'],
|
|
388
|
+
stroke: ['strokeStyle'],
|
|
389
|
+
width: ['dimension'],
|
|
390
|
+
},
|
|
391
|
+
gradient: {
|
|
392
|
+
color: ['color'],
|
|
393
|
+
position: ['number'],
|
|
394
|
+
},
|
|
395
|
+
shadow: {
|
|
396
|
+
color: ['color'],
|
|
397
|
+
offsetX: ['dimension'],
|
|
398
|
+
offsetY: ['dimension'],
|
|
399
|
+
blur: ['dimension'],
|
|
400
|
+
spread: ['dimension'],
|
|
401
|
+
inset: ['boolean'],
|
|
402
|
+
},
|
|
403
|
+
strokeStyle: {
|
|
404
|
+
dashArray: ['dimension'],
|
|
405
|
+
},
|
|
406
|
+
transition: {
|
|
407
|
+
duration: ['duration'],
|
|
408
|
+
delay: ['duration'],
|
|
409
|
+
timingFunction: ['cubicBezier'],
|
|
410
|
+
},
|
|
411
|
+
typography: {
|
|
412
|
+
fontFamily: ['fontFamily'],
|
|
413
|
+
fontWeight: ['fontWeight'],
|
|
414
|
+
fontSize: ['dimension'],
|
|
415
|
+
lineHeight: ['dimension', 'number'],
|
|
416
|
+
letterSpacing: ['dimension'],
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolve DTCG aliases
|
|
422
|
+
*/
|
|
423
|
+
export function resolveAliases(
|
|
424
|
+
tokens: TokenNormalizedSet,
|
|
425
|
+
{ logger, refMap, sources }: { logger: Logger; refMap: RefMap; sources: Record<string, InputSource> },
|
|
426
|
+
): void {
|
|
427
|
+
for (const token of Object.values(tokens)) {
|
|
428
|
+
const aliasEntry = {
|
|
429
|
+
group: 'parser' as const,
|
|
430
|
+
label: 'init',
|
|
431
|
+
src: sources[token.source.filename!]?.src,
|
|
432
|
+
node: getObjMember(token.source.node, '$value'),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
for (const mode of Object.keys(token.mode)) {
|
|
436
|
+
function resolveInner(alias: string, refChain: string[]): string {
|
|
437
|
+
const nextRef = aliasToRef(alias, mode)?.$ref!;
|
|
438
|
+
if (refChain.includes(nextRef)) {
|
|
439
|
+
logger.error({ ...aliasEntry, message: 'Circular alias detected.' });
|
|
440
|
+
}
|
|
441
|
+
const nextJSONID = nextRef.replace(/\/(\$value|\$extensions).*/, '');
|
|
442
|
+
const nextToken = tokens[nextJSONID]?.mode[mode] || tokens[nextJSONID]?.mode['.'];
|
|
443
|
+
if (!nextToken) {
|
|
444
|
+
logger.error({ ...aliasEntry, message: `Could not resolve alias ${alias}.` });
|
|
445
|
+
}
|
|
446
|
+
refChain.push(nextRef);
|
|
447
|
+
if (isAlias(nextToken!.originalValue! as string)) {
|
|
448
|
+
return resolveInner(nextToken!.originalValue! as string, refChain);
|
|
449
|
+
}
|
|
450
|
+
return nextJSONID;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function traverseAndResolve(
|
|
454
|
+
value: any,
|
|
455
|
+
{ node, expectedTypes, path }: { node: momoa.AnyNode; expectedTypes?: string[]; path: (string | number)[] },
|
|
456
|
+
): any {
|
|
457
|
+
if (typeof value !== 'string') {
|
|
458
|
+
if (Array.isArray(value)) {
|
|
459
|
+
for (let i = 0; i < value.length; i++) {
|
|
460
|
+
if (!value[i]) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
value[i] = traverseAndResolve(value[i], {
|
|
464
|
+
node: (node as momoa.ArrayNode).elements?.[i]?.value!,
|
|
465
|
+
// special case: cubicBezier
|
|
466
|
+
expectedTypes: expectedTypes?.includes('cubicBezier') ? ['number'] : expectedTypes,
|
|
467
|
+
path: [...path, i],
|
|
468
|
+
}).$value;
|
|
469
|
+
}
|
|
470
|
+
} else if (typeof value === 'object') {
|
|
471
|
+
for (const key of Object.keys(value)) {
|
|
472
|
+
if (!expectedTypes?.length || !EXPECTED_NESTED_ALIAS[expectedTypes[0]!]) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
value[key] = traverseAndResolve(value[key], {
|
|
476
|
+
node: getObjMember(node as momoa.ObjectNode, key)!,
|
|
477
|
+
expectedTypes: EXPECTED_NESTED_ALIAS[expectedTypes[0]!]![key],
|
|
478
|
+
path: [...path, key],
|
|
479
|
+
}).$value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return { $value: value };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!isAlias(value)) {
|
|
486
|
+
if (!expectedTypes?.includes('string') && (value.includes('{') || value.includes('}'))) {
|
|
487
|
+
logger.error({ ...aliasEntry, message: 'Invalid alias syntax.', node });
|
|
488
|
+
}
|
|
489
|
+
return { $value: value };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const refChain: string[] = [];
|
|
493
|
+
const resolvedID = resolveInner(value, refChain);
|
|
494
|
+
if (expectedTypes?.length && !expectedTypes.includes(tokens[resolvedID]!.$type)) {
|
|
495
|
+
logger.error({
|
|
496
|
+
...aliasEntry,
|
|
497
|
+
message: `Cannot alias to $type "${tokens[resolvedID]!.$type}" from $type "${expectedTypes.join(' / ')}".`,
|
|
498
|
+
node,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
refMap[path.join('/')] = { filename: token.source.filename!, refChain };
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
$type: tokens[resolvedID]!.$type,
|
|
506
|
+
$value: tokens[resolvedID]!.mode[mode]?.$value || tokens[resolvedID]!.$value,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// resolve DTCG aliases without
|
|
511
|
+
const pathBase = mode === '.' ? token.jsonID : `${token.jsonID}/$extensions/mode/${mode}`;
|
|
512
|
+
const { $type, $value } = traverseAndResolve(token.mode[mode]!.$value, {
|
|
513
|
+
node: aliasEntry.node!,
|
|
514
|
+
expectedTypes: token.$type ? [token.$type] : undefined,
|
|
515
|
+
path: [pathBase, '$value'],
|
|
516
|
+
});
|
|
517
|
+
if (!token.$type) {
|
|
518
|
+
(token as any).$type = $type;
|
|
519
|
+
}
|
|
520
|
+
if ($value) {
|
|
521
|
+
token.mode[mode]!.$value = $value;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// fill in $type and $value
|
|
525
|
+
if (mode === '.') {
|
|
526
|
+
token.$value = token.mode[mode]!.$value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|