@terrazzo/parser 0.10.3 → 2.0.0-alpha.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/CHANGELOG.md +6 -0
- package/dist/index.d.ts +82 -333
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2203 -3660
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/build/index.ts +32 -41
- package/src/config.ts +13 -6
- package/src/lib/code-frame.ts +5 -2
- 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 -318
- 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 +76 -10
- package/src/parse/alias.ts +0 -369
- package/src/parse/json.ts +0 -211
- package/src/parse/validate.ts +0 -961
package/src/parse/alias.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import type { AnyNode, ArrayNode, ObjectNode } from '@humanwhocodes/momoa';
|
|
2
|
-
import {
|
|
3
|
-
type BorderTokenNormalized,
|
|
4
|
-
type GradientTokenNormalized,
|
|
5
|
-
isAlias,
|
|
6
|
-
parseAlias,
|
|
7
|
-
type ShadowTokenNormalized,
|
|
8
|
-
type StrokeStyleTokenNormalized,
|
|
9
|
-
type TokenNormalized,
|
|
10
|
-
type TokenNormalizedSet,
|
|
11
|
-
type TransitionTokenNormalized,
|
|
12
|
-
type TypographyTokenNormalized,
|
|
13
|
-
} from '@terrazzo/token-tools';
|
|
14
|
-
import type Logger from '../logger.js';
|
|
15
|
-
import { getObjMembers } from './json.js';
|
|
16
|
-
|
|
17
|
-
export interface ApplyAliasOptions {
|
|
18
|
-
tokensSet: TokenNormalizedSet;
|
|
19
|
-
filename: URL;
|
|
20
|
-
src: string;
|
|
21
|
-
node: ObjectNode;
|
|
22
|
-
logger: Logger;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type PreAliased<T extends TokenNormalized> = {
|
|
26
|
-
$value: T['$value'] | string;
|
|
27
|
-
mode: Record<string, T['mode'][string] & { $value: T['$value'] | string }>;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolve aliases and update the token nodes.
|
|
32
|
-
*
|
|
33
|
-
* Data structures are in an awkward in-between phase, where they have
|
|
34
|
-
* placeholders for data but we still need to resolve everything. As such,
|
|
35
|
-
* TypeScript will raise errors expecting the final shape.
|
|
36
|
-
*
|
|
37
|
-
* This is also a bit tricky because different token types alias slightly
|
|
38
|
-
* differently. For example, color tokens and other “primitive” tokens behave
|
|
39
|
-
* as-expected. But composite tokens like Typography, Gradient, Border, etc. can
|
|
40
|
-
* either fully- or partially-alias their values. Then we add modes to the mix,
|
|
41
|
-
* and we have to do the work all over again for each mode declared.
|
|
42
|
-
*
|
|
43
|
-
* All that to say, there are a generous amount of TypeScript overrides here rather
|
|
44
|
-
* than try to codify indeterminate shapes.
|
|
45
|
-
*/
|
|
46
|
-
export default function applyAliases(token: TokenNormalized, options: ApplyAliasOptions): void {
|
|
47
|
-
// prepopulate default mode (if not set)
|
|
48
|
-
token.mode['.'] ??= {} as any;
|
|
49
|
-
token.mode['.'].$value = token.$value;
|
|
50
|
-
token.mode['.'].originalValue ??= token.originalValue.$value;
|
|
51
|
-
token.mode['.'].source ??= token.source;
|
|
52
|
-
|
|
53
|
-
// resolve root
|
|
54
|
-
if (typeof token.$value === 'string' && isAlias(token.$value)) {
|
|
55
|
-
const { aliasChain, resolvedToken } = resolveAlias(token.$value, { ...options, token });
|
|
56
|
-
token.aliasOf = resolvedToken.id;
|
|
57
|
-
token.aliasChain = aliasChain;
|
|
58
|
-
(token as any).$value = resolvedToken.$value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// resolve modes
|
|
62
|
-
for (const mode of Object.keys(token.mode)) {
|
|
63
|
-
const modeValue = token.mode[mode]!.$value;
|
|
64
|
-
|
|
65
|
-
// if the entire mode value is a simple alias, resolve & continue
|
|
66
|
-
if (typeof modeValue === 'string' && isAlias(modeValue)) {
|
|
67
|
-
const expectedType = [token.$type];
|
|
68
|
-
const { aliasChain, resolvedToken } = resolveAlias(modeValue, {
|
|
69
|
-
...options,
|
|
70
|
-
token,
|
|
71
|
-
expectedType,
|
|
72
|
-
node: token.mode[mode]!.source?.node || options.node,
|
|
73
|
-
});
|
|
74
|
-
token.mode[mode]!.aliasOf = resolvedToken.id;
|
|
75
|
-
token.mode[mode]!.aliasChain = aliasChain;
|
|
76
|
-
(token.mode[mode] as any).$value = resolvedToken.$value;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// object types: expand default $value into current mode
|
|
81
|
-
if (
|
|
82
|
-
typeof token.$value === 'object' &&
|
|
83
|
-
typeof token.mode[mode]!.$value === 'object' &&
|
|
84
|
-
!Array.isArray(token.$value)
|
|
85
|
-
) {
|
|
86
|
-
for (const [k, v] of Object.entries(token.$value)) {
|
|
87
|
-
if (!(k in token.mode[mode]!.$value)) {
|
|
88
|
-
(token.mode[mode]!.$value as any)[k] = v;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// if the mode is an object or array that’s partially aliased, do work per-token type
|
|
94
|
-
const node = (getObjMembers(options.node).$value as any) || options.node;
|
|
95
|
-
switch (token.$type) {
|
|
96
|
-
case 'border': {
|
|
97
|
-
applyBorderPartialAlias(token, mode, { ...options, node });
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
case 'gradient': {
|
|
101
|
-
applyGradientPartialAlias(token, mode, { ...options, node });
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
case 'shadow': {
|
|
105
|
-
applyShadowPartialAlias(token, mode, { ...options, node });
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
case 'strokeStyle': {
|
|
109
|
-
applyStrokeStylePartialAlias(token, mode, { ...options, node });
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
case 'transition': {
|
|
113
|
-
applyTransitionPartialAlias(token, mode, { ...options, node });
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
case 'typography': {
|
|
117
|
-
applyTypographyPartialAlias(token, mode, { ...options, node });
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const LIST_FORMAT = new Intl.ListFormat('en-us', { type: 'disjunction' });
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Resolve alias. Also add info on root node if it’s the root token (has .id)
|
|
128
|
-
*/
|
|
129
|
-
function resolveAlias(alias: string, options: { token: TokenNormalized; expectedType?: string[] } & ApplyAliasOptions) {
|
|
130
|
-
const baseMessage = {
|
|
131
|
-
group: 'parser' as const,
|
|
132
|
-
label: 'alias',
|
|
133
|
-
node: options?.node,
|
|
134
|
-
filename: options.filename,
|
|
135
|
-
src: options.src,
|
|
136
|
-
};
|
|
137
|
-
const { logger, token, tokensSet } = options;
|
|
138
|
-
const shallowAliasID = parseAlias(alias);
|
|
139
|
-
const { token: resolvedToken, chain } = _resolveAliasInner(shallowAliasID, options);
|
|
140
|
-
|
|
141
|
-
// Apply missing $types while resolving
|
|
142
|
-
if (!tokensSet[token.id]!.$type) {
|
|
143
|
-
tokensSet[token.id]!.$type = resolvedToken!.$type;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// throw error if expectedType differed
|
|
147
|
-
const expectedType = [...(options.expectedType ?? [])];
|
|
148
|
-
if (token.$type && !expectedType?.length) {
|
|
149
|
-
expectedType.push(token.$type);
|
|
150
|
-
}
|
|
151
|
-
if (expectedType?.length && !expectedType.includes(resolvedToken!.$type)) {
|
|
152
|
-
logger.error({
|
|
153
|
-
...baseMessage,
|
|
154
|
-
message: `Invalid alias: expected $type: ${LIST_FORMAT.format(expectedType!)}, received $type: ${resolvedToken!.$type}.`,
|
|
155
|
-
node: (options.node?.type === 'Object' && getObjMembers(options.node).$value) || baseMessage.node,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Apply reverse aliases as we’re traversing the graph
|
|
160
|
-
if (chain?.length && resolvedToken) {
|
|
161
|
-
let needsSort = false;
|
|
162
|
-
for (const id of chain) {
|
|
163
|
-
if (id !== resolvedToken.id && !resolvedToken.aliasedBy?.includes(id)) {
|
|
164
|
-
resolvedToken.aliasedBy ??= [];
|
|
165
|
-
resolvedToken.aliasedBy!.push(id);
|
|
166
|
-
needsSort = true;
|
|
167
|
-
}
|
|
168
|
-
if (token && !resolvedToken.aliasedBy?.includes(token.id)) {
|
|
169
|
-
resolvedToken.aliasedBy ??= [];
|
|
170
|
-
resolvedToken.aliasedBy!.push(token.id);
|
|
171
|
-
needsSort = true;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (needsSort) {
|
|
175
|
-
resolvedToken.aliasedBy!.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return { resolvedToken: resolvedToken!, aliasChain: chain };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function _resolveAliasInner(
|
|
182
|
-
alias: string,
|
|
183
|
-
{
|
|
184
|
-
scanned = [],
|
|
185
|
-
...options
|
|
186
|
-
}: {
|
|
187
|
-
tokensSet: Record<string, TokenNormalized>;
|
|
188
|
-
logger: Logger;
|
|
189
|
-
filename?: URL;
|
|
190
|
-
src: string;
|
|
191
|
-
node: AnyNode;
|
|
192
|
-
scanned?: string[];
|
|
193
|
-
},
|
|
194
|
-
): { token: TokenNormalized; chain: string[] } {
|
|
195
|
-
const { logger, filename, src, node, tokensSet } = options;
|
|
196
|
-
const baseMessage = { group: 'parser' as const, label: 'alias', filename, src, node };
|
|
197
|
-
const id = parseAlias(alias);
|
|
198
|
-
if (!tokensSet[id]) {
|
|
199
|
-
logger.error({ ...baseMessage, message: `Alias {${alias}} not found.` });
|
|
200
|
-
}
|
|
201
|
-
if (scanned.includes(id)) {
|
|
202
|
-
logger.error({ ...baseMessage, message: `Circular alias detected from ${alias}.` });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const token = tokensSet[id]!;
|
|
206
|
-
scanned.push(id);
|
|
207
|
-
|
|
208
|
-
// important: use originalValue to trace the full alias path correctly
|
|
209
|
-
// finish resolution
|
|
210
|
-
if (typeof token.originalValue.$value !== 'string' || !isAlias(token.originalValue.$value)) {
|
|
211
|
-
return { token, chain: scanned };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// continue resolving
|
|
215
|
-
return _resolveAliasInner(token.originalValue.$value as string, { ...options, scanned });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function applyBorderPartialAlias(token: BorderTokenNormalized, mode: string, options: ApplyAliasOptions): void {
|
|
219
|
-
for (const [k, v] of Object.entries(token.mode[mode]!.$value)) {
|
|
220
|
-
if (typeof v === 'string' && isAlias(v)) {
|
|
221
|
-
token.mode[mode]!.partialAliasOf ??= {};
|
|
222
|
-
const node = (getObjMembers(options.node)[k] as any) || options.node;
|
|
223
|
-
const { resolvedToken } = resolveAlias(v, {
|
|
224
|
-
...options,
|
|
225
|
-
token,
|
|
226
|
-
expectedType: { color: ['color'], width: ['dimension'], style: ['strokeStyle'] }[k],
|
|
227
|
-
node,
|
|
228
|
-
});
|
|
229
|
-
(token.mode[mode]!.partialAliasOf as any)[k] = parseAlias(v);
|
|
230
|
-
if (mode === '.') {
|
|
231
|
-
token.partialAliasOf ??= {};
|
|
232
|
-
(token.partialAliasOf as any)[k] = parseAlias(v);
|
|
233
|
-
}
|
|
234
|
-
(token.mode[mode]!.$value as any)[k] = resolvedToken.$value;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function applyGradientPartialAlias(token: GradientTokenNormalized, mode: string, options: ApplyAliasOptions): void {
|
|
240
|
-
for (let i = 0; i < token.mode[mode]!.$value.length; i++) {
|
|
241
|
-
const step = token.mode[mode]!.$value[i]!;
|
|
242
|
-
for (const [k, v] of Object.entries(step)) {
|
|
243
|
-
if (typeof v === 'string' && isAlias(v)) {
|
|
244
|
-
token.mode[mode]!.partialAliasOf ??= [];
|
|
245
|
-
(token.mode[mode]!.partialAliasOf as any)[i] ??= {};
|
|
246
|
-
const expectedType = { color: ['color'], position: ['number'] }[k];
|
|
247
|
-
let node = ((options.node as unknown as ArrayNode | undefined)?.elements?.[i]?.value as any) || options.node;
|
|
248
|
-
if (node.type === 'Object') {
|
|
249
|
-
node = getObjMembers(node)[k] || node;
|
|
250
|
-
}
|
|
251
|
-
const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node });
|
|
252
|
-
(token.mode[mode]!.partialAliasOf[i] as any)[k] = parseAlias(v);
|
|
253
|
-
if (mode === '.') {
|
|
254
|
-
token.partialAliasOf ??= [];
|
|
255
|
-
(token.partialAliasOf as any)[i] ??= {};
|
|
256
|
-
(token.partialAliasOf[i] as any)[k] = parseAlias(v);
|
|
257
|
-
}
|
|
258
|
-
(step as any)[k] = resolvedToken.$value;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function applyShadowPartialAlias(token: ShadowTokenNormalized, mode: string, options: ApplyAliasOptions): void {
|
|
265
|
-
// shadow-only fix: historically this token type may or may not allow an array
|
|
266
|
-
// of values, and at this stage in parsing, they all might not have been
|
|
267
|
-
// normalized yet.
|
|
268
|
-
if (!Array.isArray(token.mode[mode]!.$value)) {
|
|
269
|
-
token.mode[mode]!.$value = [token.mode[mode]!.$value];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
for (let i = 0; i < token.mode[mode]!.$value.length; i++) {
|
|
273
|
-
const layer = token.mode[mode]!.$value[i]!;
|
|
274
|
-
for (const [k, v] of Object.entries(layer)) {
|
|
275
|
-
if (typeof v === 'string' && isAlias(v)) {
|
|
276
|
-
token.mode[mode]!.partialAliasOf ??= [];
|
|
277
|
-
token.mode[mode]!.partialAliasOf[i] ??= {};
|
|
278
|
-
const expectedType = {
|
|
279
|
-
offsetX: ['dimension'],
|
|
280
|
-
offsetY: ['dimension'],
|
|
281
|
-
blur: ['dimension'],
|
|
282
|
-
spread: ['dimension'],
|
|
283
|
-
color: ['color'],
|
|
284
|
-
inset: ['boolean'],
|
|
285
|
-
}[k];
|
|
286
|
-
let node = ((options.node as unknown as ArrayNode | undefined)?.elements?.[i] as any) || options.node;
|
|
287
|
-
if (node.type === 'Object') {
|
|
288
|
-
node = getObjMembers(node)[k] || node;
|
|
289
|
-
}
|
|
290
|
-
const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node });
|
|
291
|
-
(token.mode[mode]!.partialAliasOf[i] as any)[k] = parseAlias(v);
|
|
292
|
-
if (mode === '.') {
|
|
293
|
-
token.partialAliasOf ??= [];
|
|
294
|
-
token.partialAliasOf[i] ??= {};
|
|
295
|
-
(token.partialAliasOf[i] as any)[k] = parseAlias(v);
|
|
296
|
-
}
|
|
297
|
-
(layer as any)[k] = resolvedToken.$value;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function applyStrokeStylePartialAlias(
|
|
304
|
-
token: StrokeStyleTokenNormalized,
|
|
305
|
-
mode: string,
|
|
306
|
-
options: ApplyAliasOptions,
|
|
307
|
-
): void {
|
|
308
|
-
// only dashArray can be aliased
|
|
309
|
-
if (typeof token.mode[mode]!.$value !== 'object' || !('dashArray' in token.mode[mode]!.$value)) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
for (let i = 0; i < token.mode[mode]!.$value.dashArray.length; i++) {
|
|
314
|
-
const dash = token.mode[mode]!.$value.dashArray[i]!;
|
|
315
|
-
if (typeof dash === 'string' && isAlias(dash)) {
|
|
316
|
-
let node = (getObjMembers(options.node).dashArray as any) || options.node;
|
|
317
|
-
if (node.type === 'Array') {
|
|
318
|
-
node = ((node as unknown as ArrayNode | undefined)?.elements?.[i]?.value as any) || node;
|
|
319
|
-
}
|
|
320
|
-
const { resolvedToken } = resolveAlias(dash, {
|
|
321
|
-
...options,
|
|
322
|
-
token,
|
|
323
|
-
expectedType: ['dimension'],
|
|
324
|
-
node,
|
|
325
|
-
});
|
|
326
|
-
(token.mode[mode]!.$value as any).dashArray[i] = resolvedToken.$value;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function applyTransitionPartialAlias(token: TransitionTokenNormalized, mode: string, options: ApplyAliasOptions): void {
|
|
332
|
-
for (const [k, v] of Object.entries(token.mode[mode]!.$value)) {
|
|
333
|
-
if (typeof v === 'string' && isAlias(v)) {
|
|
334
|
-
token.mode[mode]!.partialAliasOf ??= {};
|
|
335
|
-
const expectedType = { duration: ['duration'], delay: ['duration'], timingFunction: ['cubicBezier'] }[k];
|
|
336
|
-
const node = (getObjMembers(options.node)[k] as any) || options.node;
|
|
337
|
-
const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node });
|
|
338
|
-
(token.mode[mode]!.partialAliasOf as any)[k] = parseAlias(v);
|
|
339
|
-
if (mode === '.') {
|
|
340
|
-
token.partialAliasOf ??= {};
|
|
341
|
-
(token.partialAliasOf as any)[k] = parseAlias(v);
|
|
342
|
-
}
|
|
343
|
-
(token.mode[mode]!.$value as any)[k] = resolvedToken.$value;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function applyTypographyPartialAlias(token: TypographyTokenNormalized, mode: string, options: ApplyAliasOptions): void {
|
|
349
|
-
for (const [k, v] of Object.entries(token.mode[mode]!.$value)) {
|
|
350
|
-
if (typeof v === 'string' && isAlias(v)) {
|
|
351
|
-
token.partialAliasOf ??= {};
|
|
352
|
-
token.mode[mode]!.partialAliasOf ??= {};
|
|
353
|
-
const expectedType = {
|
|
354
|
-
fontFamily: ['fontFamily'],
|
|
355
|
-
fontSize: ['dimension'],
|
|
356
|
-
fontWeight: ['fontWeight'],
|
|
357
|
-
letterSpacing: ['dimension'],
|
|
358
|
-
lineHeight: ['dimension', 'number'],
|
|
359
|
-
}[k] || ['string'];
|
|
360
|
-
const node = (getObjMembers(options.node)[k] as any) || options.node;
|
|
361
|
-
const { resolvedToken } = resolveAlias(v, { ...options, token, expectedType, node });
|
|
362
|
-
(token.mode[mode]!.partialAliasOf as any)[k] = parseAlias(v);
|
|
363
|
-
if (mode === '.') {
|
|
364
|
-
token.partialAliasOf[k] = parseAlias(v);
|
|
365
|
-
}
|
|
366
|
-
(token.mode[mode]!.$value as any)[k] = resolvedToken.$value;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
package/src/parse/json.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
type DocumentNode,
|
|
4
|
-
type MemberNode,
|
|
5
|
-
parse as momoaParse,
|
|
6
|
-
type ObjectNode,
|
|
7
|
-
type ParseOptions,
|
|
8
|
-
print,
|
|
9
|
-
type ValueNode,
|
|
10
|
-
} from '@humanwhocodes/momoa';
|
|
11
|
-
import type yamlToMomoa from 'yaml-to-momoa';
|
|
12
|
-
import type Logger from '../logger.js';
|
|
13
|
-
import type { InputSource } from '../types.js';
|
|
14
|
-
|
|
15
|
-
export interface JSONVisitor {
|
|
16
|
-
enter?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
17
|
-
exit?: (node: AnyNode, parent: AnyNode | undefined, path: string[]) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const CHILD_KEYS = {
|
|
21
|
-
Document: ['body'] as const,
|
|
22
|
-
Object: ['members'] as const,
|
|
23
|
-
Member: ['name', 'value'] as const,
|
|
24
|
-
Element: ['value'] as const,
|
|
25
|
-
Array: ['elements'] as const,
|
|
26
|
-
String: [] as const,
|
|
27
|
-
Number: [] as const,
|
|
28
|
-
Boolean: [] as const,
|
|
29
|
-
Null: [] as const,
|
|
30
|
-
Identifier: [] as const,
|
|
31
|
-
NaN: [] as const,
|
|
32
|
-
Infinity: [] as const,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** Determines if a given value is an AST node. */
|
|
36
|
-
export function isNode(value: unknown): boolean {
|
|
37
|
-
return !!value && typeof value === 'object' && 'type' in value && typeof value.type === 'string';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type ValueNodeWithIndex = ValueNode & { index: number };
|
|
41
|
-
|
|
42
|
-
/** Get ObjectNode members as object */
|
|
43
|
-
export function getObjMembers(node: ObjectNode): Record<string | number, ValueNodeWithIndex> {
|
|
44
|
-
const members: Record<string | number, ValueNodeWithIndex> = {};
|
|
45
|
-
if (node.type !== 'Object') {
|
|
46
|
-
return members;
|
|
47
|
-
}
|
|
48
|
-
for (let i = 0; i < node.members.length; i++) {
|
|
49
|
-
const m = node.members[i]!;
|
|
50
|
-
if (m.name.type !== 'String') {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
members[m.name.value] = { ...m.value, index: i };
|
|
54
|
-
}
|
|
55
|
-
return members;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Inject members to ObjectNode */
|
|
59
|
-
export function injectObjMembers(node: ObjectNode, members: MemberNode[] = []) {
|
|
60
|
-
if (node.type !== 'Object') {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
node.members.push(...members);
|
|
64
|
-
}
|
|
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
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Variation of Momoa’s traverse(), which keeps track of global path.
|
|
73
|
-
* Allows mutation of AST (along with any consequences)
|
|
74
|
-
*/
|
|
75
|
-
export function traverse(root: AnyNode, visitor: JSONVisitor) {
|
|
76
|
-
/**
|
|
77
|
-
* Recursively visits a node.
|
|
78
|
-
* @param {AnyNode} node The node to visit.
|
|
79
|
-
* @param {AnyNode} [parent] The parent of the node to visit.
|
|
80
|
-
* @return {void}
|
|
81
|
-
*/
|
|
82
|
-
function visitNode(node: AnyNode, parent: AnyNode | undefined, path: string[] = []) {
|
|
83
|
-
const nextPath = [...path];
|
|
84
|
-
if (node.type === 'Member') {
|
|
85
|
-
const { name } = node;
|
|
86
|
-
nextPath.push('value' in name ? name.value : String(name));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
visitor.enter?.(node, parent, nextPath);
|
|
90
|
-
|
|
91
|
-
const childNode = CHILD_KEYS[node.type];
|
|
92
|
-
for (const key of childNode ?? []) {
|
|
93
|
-
const value = node[key as keyof typeof node];
|
|
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);
|
|
100
|
-
}
|
|
101
|
-
} else if (isNode(value)) {
|
|
102
|
-
visitNode(value as unknown as AnyNode, node, nextPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
visitor.exit?.(node, parent, nextPath);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
visitNode(root, undefined, []);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Determine if an input is likely a JSON string */
|
|
113
|
-
export function maybeRawJSON(input: string): boolean {
|
|
114
|
-
return typeof input === 'string' && input.trim().startsWith('{');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Find Momoa node by traversing paths */
|
|
118
|
-
export function findNode<T = AnyNode>(node: AnyNode, path: string[]): T | undefined {
|
|
119
|
-
if (!path.length) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let nextNode: AnyNode | undefined;
|
|
124
|
-
|
|
125
|
-
switch (node.type) {
|
|
126
|
-
// for Document nodes, dive into body for “free” (not part of the path)
|
|
127
|
-
case 'Document': {
|
|
128
|
-
return findNode(node.body, path);
|
|
129
|
-
}
|
|
130
|
-
case 'Object': {
|
|
131
|
-
const [member, ...rest] = path;
|
|
132
|
-
nextNode = node.members.find((m) => m.name.type === 'String' && m.name.value === member)?.value;
|
|
133
|
-
if (nextNode && rest.length) {
|
|
134
|
-
return findNode(nextNode, path.slice(1));
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
case 'Array': {
|
|
139
|
-
const [_index, ...rest] = path;
|
|
140
|
-
const index = Number.parseInt(_index!, 10);
|
|
141
|
-
nextNode = node.elements[index]?.value;
|
|
142
|
-
if (nextNode && rest.length) {
|
|
143
|
-
return findNode(nextNode, path.slice(1));
|
|
144
|
-
}
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return nextNode as T;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface ToMomoaOptions {
|
|
153
|
-
filename?: URL;
|
|
154
|
-
continueOnError?: boolean;
|
|
155
|
-
logger: Logger;
|
|
156
|
-
yamlToMomoa?: typeof yamlToMomoa;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function toMomoa(
|
|
160
|
-
input: string | Record<string, any>,
|
|
161
|
-
{ continueOnError, filename, logger, yamlToMomoa }: ToMomoaOptions,
|
|
162
|
-
): InputSource {
|
|
163
|
-
let src = '';
|
|
164
|
-
if (typeof input === 'string') {
|
|
165
|
-
src = input;
|
|
166
|
-
}
|
|
167
|
-
let document = {} as DocumentNode;
|
|
168
|
-
if (typeof input === 'string' && !maybeRawJSON(input)) {
|
|
169
|
-
if (yamlToMomoa) {
|
|
170
|
-
try {
|
|
171
|
-
document = yamlToMomoa(input); // if string, but not JSON, attempt YAML
|
|
172
|
-
} catch (err) {
|
|
173
|
-
logger.error({ group: 'parser', label: 'json', message: String(err), filename, src: input, continueOnError });
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
logger.error({
|
|
177
|
-
group: 'parser',
|
|
178
|
-
label: 'yaml',
|
|
179
|
-
message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
|
|
180
|
-
|
|
181
|
-
import { parse } from '@terrazzo/parser';
|
|
182
|
-
import yamlToMomoa from 'yaml-to-momoa';
|
|
183
|
-
|
|
184
|
-
parse(yamlString, { yamlToMomoa });`,
|
|
185
|
-
continueOnError: false, // fail here; no point in continuing
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
document = parseJSON(input);
|
|
190
|
-
}
|
|
191
|
-
if (!src) {
|
|
192
|
-
src = print(document, { indent: 2 });
|
|
193
|
-
}
|
|
194
|
-
return { src, document };
|
|
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
|
-
}
|