@terrazzo/parser 0.1.3 → 0.2.0
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +0 -12
- package/dist/build/index.d.ts +19 -0
- package/dist/build/index.js +165 -0
- package/dist/build/index.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +269 -0
- package/dist/config.js.map +1 -0
- package/{index.d.ts → dist/index.d.ts} +1 -5
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/code-frame.d.ts +30 -0
- package/dist/lib/code-frame.js +108 -0
- package/dist/lib/code-frame.js.map +1 -0
- package/dist/lint/index.d.ts +11 -0
- package/dist/lint/index.js +102 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/plugin-core/index.d.ts +12 -0
- package/dist/lint/plugin-core/index.js +40 -0
- package/dist/lint/plugin-core/index.js.map +1 -0
- package/dist/lint/plugin-core/lib/docs.d.ts +1 -0
- package/dist/lint/plugin-core/lib/docs.js +4 -0
- package/dist/lint/plugin-core/lib/docs.js.map +1 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.d.ts +39 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js +58 -0
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js.map +1 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.d.ts +13 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js +45 -0
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js.map +1 -0
- package/dist/lint/plugin-core/rules/colorspace.d.ts +14 -0
- package/dist/lint/plugin-core/rules/colorspace.js +85 -0
- package/dist/lint/plugin-core/rules/colorspace.js.map +1 -0
- package/dist/lint/plugin-core/rules/consistent-naming.d.ts +11 -0
- package/dist/lint/plugin-core/rules/consistent-naming.js +49 -0
- package/dist/lint/plugin-core/rules/consistent-naming.js.map +1 -0
- package/dist/lint/plugin-core/rules/descriptions.d.ts +9 -0
- package/dist/lint/plugin-core/rules/descriptions.js +32 -0
- package/dist/lint/plugin-core/rules/descriptions.js.map +1 -0
- package/dist/lint/plugin-core/rules/duplicate-values.d.ts +9 -0
- package/dist/lint/plugin-core/rules/duplicate-values.js +65 -0
- package/dist/lint/plugin-core/rules/duplicate-values.js.map +1 -0
- package/dist/lint/plugin-core/rules/max-gamut.d.ts +14 -0
- package/dist/lint/plugin-core/rules/max-gamut.js +101 -0
- package/dist/lint/plugin-core/rules/max-gamut.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-children.d.ts +18 -0
- package/dist/lint/plugin-core/rules/required-children.js +78 -0
- package/dist/lint/plugin-core/rules/required-children.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-modes.d.ts +13 -0
- package/dist/lint/plugin-core/rules/required-modes.js +52 -0
- package/dist/lint/plugin-core/rules/required-modes.js.map +1 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.d.ts +10 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.js +38 -0
- package/dist/lint/plugin-core/rules/required-typography-properties.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.js +123 -0
- package/dist/logger.js.map +1 -0
- package/dist/parse/alias.d.ts +51 -0
- package/dist/parse/alias.js +188 -0
- package/dist/parse/alias.js.map +1 -0
- package/dist/parse/index.d.ts +27 -0
- package/dist/parse/index.js +379 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/json.d.ts +36 -0
- package/dist/parse/json.js +88 -0
- package/dist/parse/json.js.map +1 -0
- package/dist/parse/normalize.d.ts +23 -0
- package/dist/parse/normalize.js +163 -0
- package/dist/parse/normalize.js.map +1 -0
- package/dist/parse/validate.d.ts +45 -0
- package/dist/parse/validate.js +601 -0
- package/dist/parse/validate.js.map +1 -0
- package/dist/types.d.ts +264 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -7
- package/{build/index.js → src/build/index.ts} +47 -63
- package/src/config.ts +280 -0
- package/src/index.ts +18 -0
- package/{lib/code-frame.js → src/lib/code-frame.ts} +41 -8
- package/src/lint/index.ts +135 -0
- package/src/lint/plugin-core/index.ts +47 -0
- package/src/lint/plugin-core/lib/docs.ts +3 -0
- package/src/lint/plugin-core/rules/a11y-min-contrast.ts +91 -0
- package/src/lint/plugin-core/rules/a11y-min-font-size.ts +64 -0
- package/src/lint/plugin-core/rules/colorspace.ts +101 -0
- package/src/lint/plugin-core/rules/consistent-naming.ts +65 -0
- package/src/lint/plugin-core/rules/descriptions.ts +41 -0
- package/src/lint/plugin-core/rules/duplicate-values.ts +80 -0
- package/src/lint/plugin-core/rules/max-gamut.ts +121 -0
- package/src/lint/plugin-core/rules/required-children.ts +104 -0
- package/src/lint/plugin-core/rules/required-modes.ts +71 -0
- package/src/lint/plugin-core/rules/required-typography-properties.ts +53 -0
- package/{logger.js → src/logger.ts} +55 -16
- package/src/parse/alias.ts +224 -0
- package/src/parse/index.ts +457 -0
- package/src/parse/json.ts +106 -0
- package/{parse/normalize.js → src/parse/normalize.ts} +70 -24
- package/{parse/validate.js → src/parse/validate.ts} +154 -236
- package/src/types.ts +310 -0
- package/build/index.d.ts +0 -113
- package/config.d.ts +0 -64
- package/config.js +0 -206
- package/index.js +0 -35
- package/lib/code-frame.d.ts +0 -56
- package/lint/index.d.ts +0 -44
- package/lint/index.js +0 -59
- package/lint/plugin-core/index.d.ts +0 -3
- package/lint/plugin-core/index.js +0 -12
- package/lint/plugin-core/rules/duplicate-values.d.ts +0 -10
- package/lint/plugin-core/rules/duplicate-values.js +0 -68
- package/logger.d.ts +0 -71
- package/parse/index.d.ts +0 -45
- package/parse/index.js +0 -592
- package/parse/json.d.ts +0 -30
- package/parse/json.js +0 -94
- package/parse/normalize.d.ts +0 -3
- package/parse/validate.d.ts +0 -43
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { type DocumentNode, type ObjectNode, evaluate, parse as parseJSON, print } from '@humanwhocodes/momoa';
|
|
2
|
+
import { type Token, type TokenNormalized, isTokenMatch, pluralize, splitID } from '@terrazzo/token-tools';
|
|
3
|
+
import type ytm from 'yaml-to-momoa';
|
|
4
|
+
import lintRunner from '../lint/index.js';
|
|
5
|
+
import Logger from '../logger.js';
|
|
6
|
+
import type { ConfigInit, InputSource } from '../types.js';
|
|
7
|
+
import { applyAliases } from './alias.js';
|
|
8
|
+
import { getObjMembers, injectObjMembers, maybeJSONString, traverse } from './json.js';
|
|
9
|
+
import normalize from './normalize.js';
|
|
10
|
+
import validate from './validate.js';
|
|
11
|
+
|
|
12
|
+
export * from './validate.js';
|
|
13
|
+
|
|
14
|
+
export interface ParseOptions {
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
config: ConfigInit;
|
|
17
|
+
/**
|
|
18
|
+
* Skip lint step
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
skipLint?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Continue on error? (Useful for `tz check`)
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
continueOnError?: boolean;
|
|
27
|
+
/** Provide yamlToMomoa module to parse YAML (by default, this isn’t shipped to cut down on package weight) */
|
|
28
|
+
yamlToMomoa?: typeof ytm;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ParseResult {
|
|
32
|
+
tokens: Record<string, TokenNormalized>;
|
|
33
|
+
sources: InputSource[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse */
|
|
37
|
+
export default async function parse(
|
|
38
|
+
input: Omit<InputSource, 'document'>[],
|
|
39
|
+
{
|
|
40
|
+
logger = new Logger(),
|
|
41
|
+
skipLint = false,
|
|
42
|
+
config = {} as ConfigInit,
|
|
43
|
+
continueOnError = false,
|
|
44
|
+
yamlToMomoa,
|
|
45
|
+
}: ParseOptions = {} as ParseOptions,
|
|
46
|
+
): Promise<ParseResult> {
|
|
47
|
+
let tokens: Record<string, TokenNormalized> = {};
|
|
48
|
+
// note: only keeps track of sources with locations on disk; in-memory sources are discarded
|
|
49
|
+
// (it’s only for reporting line numbers, which doesn’t mean as much for dynamic sources)
|
|
50
|
+
const sources: Record<string, InputSource> = {};
|
|
51
|
+
|
|
52
|
+
if (!Array.isArray(input)) {
|
|
53
|
+
logger.error({ group: 'parser', label: 'init', message: 'Input must be an array of input objects.' });
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < input.length; i++) {
|
|
56
|
+
if (!input[i] || typeof input[i] !== 'object') {
|
|
57
|
+
logger.error({ group: 'parser', label: 'init', message: `Input (${i}) must be an object.` });
|
|
58
|
+
}
|
|
59
|
+
if (!input[i]!.src || (typeof input[i]!.src !== 'string' && typeof input[i]!.src !== 'object')) {
|
|
60
|
+
logger.error({
|
|
61
|
+
group: 'parser',
|
|
62
|
+
label: 'init',
|
|
63
|
+
message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (input[i]!.filename && !(input[i]!.filename instanceof URL)) {
|
|
67
|
+
logger.error({
|
|
68
|
+
group: 'parser',
|
|
69
|
+
label: 'init',
|
|
70
|
+
message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await parseSingle(input[i]!.src, {
|
|
75
|
+
filename: input[i]!.filename!,
|
|
76
|
+
logger,
|
|
77
|
+
config,
|
|
78
|
+
skipLint,
|
|
79
|
+
continueOnError,
|
|
80
|
+
yamlToMomoa,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
tokens = Object.assign(tokens, result.tokens);
|
|
84
|
+
if (input[i]!.filename) {
|
|
85
|
+
sources[input[i]!.filename!.protocol === 'file:' ? input[i]!.filename!.href : input[i]!.filename!.href] = {
|
|
86
|
+
filename: input[i]!.filename,
|
|
87
|
+
src: result.src,
|
|
88
|
+
document: result.document,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const totalStart = performance.now();
|
|
94
|
+
|
|
95
|
+
// 5. Resolve aliases and populate groups
|
|
96
|
+
for (const [id, token] of Object.entries(tokens)) {
|
|
97
|
+
if (!Object.hasOwn(tokens, id)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
applyAliases(token, {
|
|
101
|
+
tokens,
|
|
102
|
+
filename: sources[token.source.loc!]?.filename!,
|
|
103
|
+
src: sources[token.source.loc!]?.src as string,
|
|
104
|
+
node: token.source.node,
|
|
105
|
+
logger,
|
|
106
|
+
});
|
|
107
|
+
token.mode['.']!.$value = token.$value;
|
|
108
|
+
if (token.aliasOf) {
|
|
109
|
+
token.mode['.']!.aliasOf = token.aliasOf;
|
|
110
|
+
}
|
|
111
|
+
if (token.partialAliasOf) {
|
|
112
|
+
token.mode['.']!.partialAliasOf = token.partialAliasOf;
|
|
113
|
+
}
|
|
114
|
+
const { group: parentGroup } = splitID(id);
|
|
115
|
+
if (parentGroup) {
|
|
116
|
+
for (const siblingID of Object.keys(tokens)) {
|
|
117
|
+
const { group: siblingGroup } = splitID(siblingID);
|
|
118
|
+
if (siblingGroup?.startsWith(parentGroup)) {
|
|
119
|
+
token.group.tokens.push(siblingID);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 6. resolve mode aliases
|
|
126
|
+
const modesStart = performance.now();
|
|
127
|
+
logger.debug({
|
|
128
|
+
group: 'parser',
|
|
129
|
+
label: 'modes',
|
|
130
|
+
message: 'Start mode resolution',
|
|
131
|
+
});
|
|
132
|
+
for (const [id, token] of Object.entries(tokens)) {
|
|
133
|
+
if (!Object.hasOwn(tokens, id)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const [mode, modeValue] of Object.entries(token.mode)) {
|
|
137
|
+
if (mode === '.') {
|
|
138
|
+
continue; // skip shadow of root value
|
|
139
|
+
}
|
|
140
|
+
applyAliases(modeValue, {
|
|
141
|
+
tokens,
|
|
142
|
+
node: modeValue.source.node,
|
|
143
|
+
logger,
|
|
144
|
+
src: sources[token.source.loc!]?.src as string,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
logger.debug({
|
|
149
|
+
group: 'parser',
|
|
150
|
+
label: 'modes',
|
|
151
|
+
message: 'Finish token modes',
|
|
152
|
+
timing: performance.now() - modesStart,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
logger.debug({
|
|
156
|
+
group: 'parser',
|
|
157
|
+
label: 'core',
|
|
158
|
+
message: 'Finish all parser tasks',
|
|
159
|
+
timing: performance.now() - totalStart,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (continueOnError) {
|
|
163
|
+
const { errorCount } = logger.stats();
|
|
164
|
+
if (errorCount > 0) {
|
|
165
|
+
logger.error({
|
|
166
|
+
message: `Parser encountered ${errorCount} ${pluralize(errorCount, 'error', 'errors')}. Exiting.`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
tokens,
|
|
173
|
+
sources: Object.values(sources),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Parse a single input */
|
|
178
|
+
async function parseSingle(
|
|
179
|
+
input: string | Record<string, any>,
|
|
180
|
+
{
|
|
181
|
+
filename,
|
|
182
|
+
logger,
|
|
183
|
+
config,
|
|
184
|
+
skipLint,
|
|
185
|
+
continueOnError = false,
|
|
186
|
+
yamlToMomoa, // optional dependency, declared here so the parser itself doesn’t have to load a heavy dep in-browser
|
|
187
|
+
}: {
|
|
188
|
+
filename: URL;
|
|
189
|
+
logger: Logger;
|
|
190
|
+
config: ConfigInit;
|
|
191
|
+
skipLint: boolean;
|
|
192
|
+
continueOnError?: boolean;
|
|
193
|
+
yamlToMomoa?: typeof ytm;
|
|
194
|
+
},
|
|
195
|
+
) {
|
|
196
|
+
// 1. Build AST
|
|
197
|
+
let src: any;
|
|
198
|
+
if (typeof input === 'string') {
|
|
199
|
+
src = input;
|
|
200
|
+
}
|
|
201
|
+
const startParsing = performance.now();
|
|
202
|
+
logger.debug({ group: 'parser', label: 'parse', message: 'Start tokens parsing' });
|
|
203
|
+
let document = {} as DocumentNode;
|
|
204
|
+
if (typeof input === 'string' && !maybeJSONString(input)) {
|
|
205
|
+
if (yamlToMomoa) {
|
|
206
|
+
try {
|
|
207
|
+
document = yamlToMomoa(input); // if string, but not JSON, attempt YAML
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logger.error({ message: String(err), filename, src: input, continueOnError });
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
logger.error({
|
|
213
|
+
group: 'parser',
|
|
214
|
+
message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
|
|
215
|
+
|
|
216
|
+
import { parse } from '@terrazzo/parser';
|
|
217
|
+
import yamlToMomoa from 'yaml-to-momoa';
|
|
218
|
+
|
|
219
|
+
parse(yamlString, { yamlToMomoa });`,
|
|
220
|
+
continueOnError: false, // fail here; no point in continuing
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
document = parseJSON(
|
|
225
|
+
typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), // everything else: assert it’s JSON-serializable
|
|
226
|
+
{
|
|
227
|
+
mode: 'jsonc',
|
|
228
|
+
ranges: true,
|
|
229
|
+
tokens: true,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (!src) {
|
|
234
|
+
src = print(document, { indent: 2 });
|
|
235
|
+
}
|
|
236
|
+
logger.debug({
|
|
237
|
+
group: 'parser',
|
|
238
|
+
label: 'parse',
|
|
239
|
+
message: 'Finish tokens parsing',
|
|
240
|
+
timing: performance.now() - startParsing,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const tokens: Record<string, TokenNormalized> = {};
|
|
244
|
+
|
|
245
|
+
// 2. Walk AST once to validate tokens
|
|
246
|
+
const startValidation = performance.now();
|
|
247
|
+
logger.debug({ group: 'parser', label: 'validate', message: 'Start tokens validation' });
|
|
248
|
+
const $typeInheritance: Record<string, Token['$type']> = {};
|
|
249
|
+
traverse(document, {
|
|
250
|
+
enter(node, parent, path) {
|
|
251
|
+
if (node.type === 'Member' && node.value.type === 'Object' && node.value.members) {
|
|
252
|
+
const members = getObjMembers(node.value);
|
|
253
|
+
|
|
254
|
+
// keep track of $types
|
|
255
|
+
if (members.$type && members.$type.type === 'String' && !members.$value) {
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
$typeInheritance[path.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const id = path.join('.');
|
|
261
|
+
|
|
262
|
+
if (members.$value) {
|
|
263
|
+
const extensions = members.$extensions ? getObjMembers(members.$extensions as ObjectNode) : undefined;
|
|
264
|
+
const sourceNode = structuredClone(node);
|
|
265
|
+
|
|
266
|
+
// get parent type by taking the closest-scoped $type (length === closer)
|
|
267
|
+
let parent$type: Token['$type'] | undefined;
|
|
268
|
+
let longestPath = '';
|
|
269
|
+
for (const [k, v] of Object.entries($typeInheritance)) {
|
|
270
|
+
if (k === '.' || id.startsWith(k)) {
|
|
271
|
+
if (k.length > longestPath.length) {
|
|
272
|
+
parent$type = v;
|
|
273
|
+
longestPath = k;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (parent$type && !members.$type) {
|
|
278
|
+
sourceNode.value = injectObjMembers(
|
|
279
|
+
// @ts-ignore
|
|
280
|
+
sourceNode.value,
|
|
281
|
+
[parent$type],
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
validate(sourceNode, { filename, src, logger });
|
|
286
|
+
|
|
287
|
+
// All tokens must be valid, so we want to validate it up till this
|
|
288
|
+
// point. However, if we are ignoring this token (or respecting
|
|
289
|
+
// $deprecated, we can omit it from the output.
|
|
290
|
+
const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
|
|
291
|
+
if (
|
|
292
|
+
(config.ignore.deprecated && $deprecated) ||
|
|
293
|
+
(config.ignore.tokens && isTokenMatch(id, config.ignore.tokens))
|
|
294
|
+
) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const group: TokenNormalized['group'] = { id: splitID(id).group!, tokens: [] };
|
|
299
|
+
if (parent$type) {
|
|
300
|
+
group.$type =
|
|
301
|
+
// @ts-ignore
|
|
302
|
+
parent$type.value.value;
|
|
303
|
+
}
|
|
304
|
+
// note: this will also include sibling tokens, so be selective about only accessing group-specific properties
|
|
305
|
+
const groupMembers = getObjMembers(
|
|
306
|
+
// @ts-ignore
|
|
307
|
+
parent,
|
|
308
|
+
);
|
|
309
|
+
if (groupMembers.$description) {
|
|
310
|
+
group.$description = evaluate(groupMembers.$description) as string;
|
|
311
|
+
}
|
|
312
|
+
if (groupMembers.$extensions) {
|
|
313
|
+
group.$extensions = evaluate(groupMembers.$extensions) as Record<string, unknown>;
|
|
314
|
+
}
|
|
315
|
+
const token: TokenNormalized = {
|
|
316
|
+
// @ts-ignore
|
|
317
|
+
$type: members.$type?.value ?? parent$type?.value.value,
|
|
318
|
+
// @ts-ignore
|
|
319
|
+
$value: evaluate(members.$value),
|
|
320
|
+
id,
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
mode: {},
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
originalValue: evaluate(node.value),
|
|
325
|
+
group,
|
|
326
|
+
source: {
|
|
327
|
+
loc: filename ? filename.href : undefined,
|
|
328
|
+
// @ts-ignore
|
|
329
|
+
node: sourceNode.value,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
// @ts-ignore
|
|
333
|
+
if (members.$description?.value) {
|
|
334
|
+
// @ts-ignore
|
|
335
|
+
token.$description = members.$description.value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// handle modes
|
|
339
|
+
// note that circular refs are avoided here, such as not duplicating `modes`
|
|
340
|
+
const modeValues = extensions?.mode
|
|
341
|
+
? getObjMembers(
|
|
342
|
+
// @ts-ignore
|
|
343
|
+
extensions.mode,
|
|
344
|
+
)
|
|
345
|
+
: {};
|
|
346
|
+
for (const mode of ['.', ...Object.keys(modeValues)]) {
|
|
347
|
+
token.mode[mode] = {
|
|
348
|
+
id: token.id,
|
|
349
|
+
// @ts-ignore
|
|
350
|
+
$type: token.$type,
|
|
351
|
+
// @ts-ignore
|
|
352
|
+
$value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
|
|
353
|
+
source: {
|
|
354
|
+
loc: filename ? filename.href : undefined,
|
|
355
|
+
// @ts-ignore
|
|
356
|
+
node: mode === '.' ? structuredClone(token.source.node) : modeValues[mode],
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
if (token.$description) {
|
|
360
|
+
token.mode[mode]!.$description = token.$description;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
tokens[id] = token;
|
|
365
|
+
} else if (!id.includes('.$value') && members.value) {
|
|
366
|
+
logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// edge case: if $type appears at root of tokens.json, collect it
|
|
371
|
+
if (node.type === 'Document' && node.body.type === 'Object' && node.body.members) {
|
|
372
|
+
const members = getObjMembers(node.body);
|
|
373
|
+
if (members.$type && members.$type.type === 'String' && !members.$value) {
|
|
374
|
+
// @ts-ignore
|
|
375
|
+
$typeInheritance['.'] = node.body.members.find((m) => m.name.value === '$type');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
logger.debug({
|
|
381
|
+
group: 'parser',
|
|
382
|
+
label: 'validate',
|
|
383
|
+
message: 'Finish tokens validation',
|
|
384
|
+
timing: performance.now() - startValidation,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// 3. normalize values
|
|
388
|
+
const normalizeStart = performance.now();
|
|
389
|
+
logger.debug({
|
|
390
|
+
group: 'parser',
|
|
391
|
+
label: 'normalize',
|
|
392
|
+
message: 'Start token normalization',
|
|
393
|
+
});
|
|
394
|
+
for (const [id, token] of Object.entries(tokens)) {
|
|
395
|
+
try {
|
|
396
|
+
tokens[id]!.$value = normalize(token);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
let { node } = token.source;
|
|
399
|
+
const members = getObjMembers(node);
|
|
400
|
+
if (members.$value) {
|
|
401
|
+
node = members.$value as ObjectNode;
|
|
402
|
+
}
|
|
403
|
+
logger.error({ message: (err as Error).message, filename, src, node, continueOnError });
|
|
404
|
+
}
|
|
405
|
+
for (const [mode, modeValue] of Object.entries(token.mode)) {
|
|
406
|
+
if (mode === '.') {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
tokens[id]!.mode[mode]!.$value = normalize(modeValue);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
let { node } = token.source;
|
|
413
|
+
const members = getObjMembers(node);
|
|
414
|
+
if (members.$value) {
|
|
415
|
+
node = members.$value as ObjectNode;
|
|
416
|
+
}
|
|
417
|
+
logger.error({
|
|
418
|
+
message: (err as Error).message,
|
|
419
|
+
filename,
|
|
420
|
+
src,
|
|
421
|
+
node: modeValue.source.node,
|
|
422
|
+
continueOnError,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 4. Execute lint runner with loaded plugins
|
|
429
|
+
if (!skipLint && config?.plugins?.length) {
|
|
430
|
+
const lintStart = performance.now();
|
|
431
|
+
logger.debug({
|
|
432
|
+
group: 'parser',
|
|
433
|
+
label: 'validate',
|
|
434
|
+
message: 'Start token linting',
|
|
435
|
+
});
|
|
436
|
+
await lintRunner({ tokens, src, config, logger });
|
|
437
|
+
logger.debug({
|
|
438
|
+
group: 'parser',
|
|
439
|
+
label: 'validate',
|
|
440
|
+
message: 'Finish token linting',
|
|
441
|
+
timing: performance.now() - lintStart,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
logger.debug({
|
|
446
|
+
group: 'parser',
|
|
447
|
+
label: 'normalize',
|
|
448
|
+
message: 'Finish token normalization',
|
|
449
|
+
timing: performance.now() - normalizeStart,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
tokens,
|
|
454
|
+
document,
|
|
455
|
+
src,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AnyNode, MemberNode, ObjectNode, ValueNode } from '@humanwhocodes/momoa';
|
|
2
|
+
|
|
3
|
+
export interface Visitor {
|
|
4
|
+
enter?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
5
|
+
exit?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const CHILD_KEYS = {
|
|
9
|
+
Document: ['body'] as const,
|
|
10
|
+
Object: ['members'] as const,
|
|
11
|
+
Member: ['name', 'value'] as const,
|
|
12
|
+
Element: ['value'] as const,
|
|
13
|
+
Array: ['elements'] as const,
|
|
14
|
+
String: [] as const,
|
|
15
|
+
Number: [] as const,
|
|
16
|
+
Boolean: [] as const,
|
|
17
|
+
Null: [] as const,
|
|
18
|
+
Identifier: [] as const,
|
|
19
|
+
NaN: [] as const,
|
|
20
|
+
Infinity: [] as const,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Determines if a given value is an AST node. */
|
|
24
|
+
export function isNode(value: unknown): boolean {
|
|
25
|
+
return !!value && typeof value === 'object' && 'type' in value && typeof value.type === 'string';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Get ObjectNode members as object */
|
|
29
|
+
export function getObjMembers(node: ObjectNode): Record<string | number, ValueNode> {
|
|
30
|
+
const members: Record<string | number, ValueNode> = {};
|
|
31
|
+
if (node.type !== 'Object') {
|
|
32
|
+
return members;
|
|
33
|
+
}
|
|
34
|
+
for (const m of node.members) {
|
|
35
|
+
if (m.name.type !== 'String') {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
members[m.name.value] = m.value;
|
|
39
|
+
}
|
|
40
|
+
return members;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inject members to ObjectNode and return a clone
|
|
45
|
+
* @param {ObjectNode} node
|
|
46
|
+
* @param {MemberNode[]} members
|
|
47
|
+
* @return {ObjectNode}
|
|
48
|
+
*/
|
|
49
|
+
export function injectObjMembers(node: ObjectNode, members: MemberNode[] = []): ObjectNode {
|
|
50
|
+
if (node.type !== 'Object') {
|
|
51
|
+
return node;
|
|
52
|
+
}
|
|
53
|
+
const newNode = structuredClone(node);
|
|
54
|
+
newNode.members.push(...members);
|
|
55
|
+
return newNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Variation of Momoa’s traverse(), which keeps track of global path
|
|
60
|
+
*/
|
|
61
|
+
export function traverse(root: AnyNode, visitor: Visitor) {
|
|
62
|
+
/**
|
|
63
|
+
* Recursively visits a node.
|
|
64
|
+
* @param {AnyNode} node The node to visit.
|
|
65
|
+
* @param {AnyNode} [parent] The parent of the node to visit.
|
|
66
|
+
* @return {void}
|
|
67
|
+
*/
|
|
68
|
+
function visitNode(node: AnyNode, parent: AnyNode | undefined, path: string[] = []) {
|
|
69
|
+
const nextPath = [...path];
|
|
70
|
+
if (node.type === 'Member') {
|
|
71
|
+
const { name } = node;
|
|
72
|
+
nextPath.push('value' in name ? name.value : String(name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visitor.enter?.(node, parent, nextPath);
|
|
76
|
+
|
|
77
|
+
const childNode = CHILD_KEYS[node.type];
|
|
78
|
+
for (const key of childNode ?? []) {
|
|
79
|
+
const value = node[key as keyof typeof node];
|
|
80
|
+
|
|
81
|
+
if (value && typeof value === 'object') {
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
for (let i = 0; i < value.length; i++) {
|
|
84
|
+
visitNode(
|
|
85
|
+
// @ts-expect-error this is safe
|
|
86
|
+
value[i],
|
|
87
|
+
node,
|
|
88
|
+
key === 'elements' ? [...nextPath, String(i)] : nextPath,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else if (isNode(value)) {
|
|
92
|
+
visitNode(value as unknown as AnyNode, node, nextPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
visitor.exit?.(node, parent, nextPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
visitNode(root, undefined, []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Determine if an input is likely a JSON string */
|
|
104
|
+
export function maybeJSONString(input: string): boolean {
|
|
105
|
+
return typeof input === 'string' && input.trim().startsWith('{');
|
|
106
|
+
}
|