@terrazzo/parser 0.0.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/LICENSE +21 -0
- package/build/index.d.ts +104 -0
- package/build/index.js +182 -0
- package/config.d.ts +64 -0
- package/config.js +196 -0
- package/index.d.ts +16 -0
- package/index.js +37 -0
- package/lint/index.d.ts +41 -0
- package/lint/index.js +59 -0
- package/lint/plugin-core/index.d.ts +3 -0
- package/lint/plugin-core/index.js +12 -0
- package/lint/plugin-core/rules/duplicate-values.d.ts +10 -0
- package/lint/plugin-core/rules/duplicate-values.js +69 -0
- package/logger.d.ts +66 -0
- package/logger.js +121 -0
- package/package.json +52 -0
- package/parse/index.d.ts +32 -0
- package/parse/index.js +372 -0
- package/parse/json.d.ts +30 -0
- package/parse/json.js +94 -0
- package/parse/normalize.d.ts +3 -0
- package/parse/normalize.js +114 -0
- package/parse/validate.d.ts +42 -0
- package/parse/validate.js +620 -0
- package/parse/yaml.d.ts +11 -0
- package/parse/yaml.js +45 -0
- package/types.d.ts +519 -0
- package/types.js +1 -0
package/parse/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { evaluate, parse as parseJSON } from '@humanwhocodes/momoa';
|
|
2
|
+
import { isAlias, parseAlias, splitID } from '@terrazzo/token-tools';
|
|
3
|
+
import lintRunner from '../lint/index.js';
|
|
4
|
+
import Logger from '../logger.js';
|
|
5
|
+
import normalize from './normalize.js';
|
|
6
|
+
import parseYAML from './yaml.js';
|
|
7
|
+
import validate from './validate.js';
|
|
8
|
+
import { getObjMembers, injectObjMembers, traverse } from './json.js';
|
|
9
|
+
|
|
10
|
+
export * from './validate.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode
|
|
14
|
+
* @typedef {import("../config.js").Plugin} Plugin
|
|
15
|
+
* @typedef {import("../types.js").TokenNormalized} TokenNormalized
|
|
16
|
+
* @typedef {object} ParseOptions
|
|
17
|
+
* @typedef {Logger} ParseOptions.logger
|
|
18
|
+
* @typedef {boolean=false} ParseOptions.skipLint
|
|
19
|
+
* @typedef {Plugin[]} ParseOptions.plugins
|
|
20
|
+
* @typedef {object} ParseResult
|
|
21
|
+
* @typedef {Record<string, TokenNormalized} ParseResult.tokens
|
|
22
|
+
* @typedef {DocumentNode} ParseResult.ast
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse
|
|
27
|
+
* @param {string | object} input
|
|
28
|
+
* @param {ParseOptions} options
|
|
29
|
+
* @return {Promise<ParseResult>}
|
|
30
|
+
*/
|
|
31
|
+
export default async function parse(input, { logger = new Logger(), skipLint = false, config } = {}) {
|
|
32
|
+
const { plugins } = config;
|
|
33
|
+
|
|
34
|
+
const totalStart = performance.now();
|
|
35
|
+
|
|
36
|
+
// 1. Build AST
|
|
37
|
+
const startParsing = performance.now();
|
|
38
|
+
logger.debug({ group: 'parser', task: 'parse', message: 'Start tokens parsing' });
|
|
39
|
+
let ast;
|
|
40
|
+
if (typeof input === 'string' && !maybeJSONString(input)) {
|
|
41
|
+
ast = parseYAML(input, { logger }); // if string, but not JSON, attempt YAML
|
|
42
|
+
} else {
|
|
43
|
+
ast = parseJSON(typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), {
|
|
44
|
+
mode: 'jsonc',
|
|
45
|
+
}); // everything else: assert it’s JSON-serializable
|
|
46
|
+
}
|
|
47
|
+
logger.debug({
|
|
48
|
+
group: 'parser',
|
|
49
|
+
task: 'parse',
|
|
50
|
+
message: 'Finish tokens parsing',
|
|
51
|
+
timing: performance.now() - startParsing,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const tokens = {};
|
|
55
|
+
|
|
56
|
+
// 2. Walk AST once to validate tokens
|
|
57
|
+
const startValidation = performance.now();
|
|
58
|
+
logger.debug({ group: 'parser', task: 'validate', message: 'Start tokens validation' });
|
|
59
|
+
let last$Type;
|
|
60
|
+
let last$TypePath = '';
|
|
61
|
+
traverse(ast, {
|
|
62
|
+
enter(node, parent, path) {
|
|
63
|
+
// reset last$Type if not in a direct ancestor tree
|
|
64
|
+
if (!last$TypePath || !path.join('.').startsWith(last$TypePath)) {
|
|
65
|
+
last$Type = undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.type === 'Member' && node.value.type === 'Object' && node.value.members) {
|
|
69
|
+
const members = getObjMembers(node.value);
|
|
70
|
+
|
|
71
|
+
// keep track of closest-scoped $type
|
|
72
|
+
// note: this is only reliable in a synchronous, single-pass traversal;
|
|
73
|
+
// otherwise we’d have to do something more complicated
|
|
74
|
+
if (members.$type && members.$type.type === 'String' && !members.$value) {
|
|
75
|
+
last$Type = node.value.members.find((m) => m.name.value === '$type');
|
|
76
|
+
last$TypePath = path.join('.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (members.$value) {
|
|
80
|
+
const extensions = members.$extensions ? getObjMembers(members.$extensions) : undefined;
|
|
81
|
+
const sourceNode = structuredClone(node);
|
|
82
|
+
if (last$Type && !members.$type) {
|
|
83
|
+
sourceNode.value = injectObjMembers(sourceNode.value, [last$Type]);
|
|
84
|
+
}
|
|
85
|
+
validate(sourceNode, { ast, logger });
|
|
86
|
+
|
|
87
|
+
const id = path.join('.');
|
|
88
|
+
const group = { id: splitID(id).group, tokens: [] };
|
|
89
|
+
if (last$Type) {
|
|
90
|
+
group.$type = last$Type.value.value;
|
|
91
|
+
}
|
|
92
|
+
// note: this will also include sibling tokens, so be selective about only accessing group-specific properties
|
|
93
|
+
const groupMembers = getObjMembers(parent);
|
|
94
|
+
if (groupMembers.$description) {
|
|
95
|
+
group.$description = evaluate(groupMembers.$description);
|
|
96
|
+
}
|
|
97
|
+
if (groupMembers.$extensions) {
|
|
98
|
+
group.$extensions = evaluate(groupMembers.$extensions);
|
|
99
|
+
}
|
|
100
|
+
const token = {
|
|
101
|
+
$type: members.$type?.value ?? last$Type?.value.value,
|
|
102
|
+
$value: evaluate(members.$value),
|
|
103
|
+
id,
|
|
104
|
+
mode: {},
|
|
105
|
+
originalValue: evaluate(node.value),
|
|
106
|
+
group,
|
|
107
|
+
sourceNode: sourceNode.value,
|
|
108
|
+
};
|
|
109
|
+
if (members.$description?.value) {
|
|
110
|
+
token.$description = members.$description.value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// handle modes
|
|
114
|
+
// note that circular refs are avoided here, such as not duplicating `modes`
|
|
115
|
+
const modeValues = extensions?.mode ? getObjMembers(extensions.mode) : {};
|
|
116
|
+
for (const mode of ['.', ...Object.keys(modeValues)]) {
|
|
117
|
+
token.mode[mode] = {
|
|
118
|
+
id: token.id,
|
|
119
|
+
$type: token.$type,
|
|
120
|
+
$value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
|
|
121
|
+
sourceNode: mode === '.' ? structuredClone(token.sourceNode) : modeValues[mode],
|
|
122
|
+
};
|
|
123
|
+
if (token.$description) {
|
|
124
|
+
token.mode[mode].$description = token.$description;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
tokens[id] = token;
|
|
129
|
+
} else if (members.value) {
|
|
130
|
+
logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, node, ast });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
logger.debug({
|
|
136
|
+
group: 'parser',
|
|
137
|
+
task: 'validate',
|
|
138
|
+
message: 'Finish tokens validation',
|
|
139
|
+
timing: performance.now() - startValidation,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 3. Execute lint runner with loaded plugins
|
|
143
|
+
if (!skipLint && plugins?.length) {
|
|
144
|
+
const lintStart = performance.now();
|
|
145
|
+
logger.debug({
|
|
146
|
+
group: 'parser',
|
|
147
|
+
task: 'validate',
|
|
148
|
+
message: 'Start token linting',
|
|
149
|
+
});
|
|
150
|
+
await lintRunner({ ast, config, logger });
|
|
151
|
+
logger.debug({
|
|
152
|
+
group: 'parser',
|
|
153
|
+
task: 'validate',
|
|
154
|
+
message: 'Finish token linting',
|
|
155
|
+
timing: performance.now() - lintStart,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. normalize values
|
|
160
|
+
const normalizeStart = performance.now();
|
|
161
|
+
logger.debug({
|
|
162
|
+
group: 'parser',
|
|
163
|
+
task: 'normalize',
|
|
164
|
+
message: 'Start token normalization',
|
|
165
|
+
});
|
|
166
|
+
for (const id in tokens) {
|
|
167
|
+
if (!Object.hasOwn(tokens, id)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
tokens[id].$value = normalize(tokens[id]);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
logger.error({ message: err.message, ast, node: tokens[id].sourceNode });
|
|
174
|
+
}
|
|
175
|
+
for (const mode in tokens[id].mode) {
|
|
176
|
+
if (mode === '.') {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
tokens[id].mode[mode].$value = normalize(tokens[id].mode[mode]);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.error({ message: err.message, ast, node: tokens[id].mode[mode].sourceNode });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
logger.debug({
|
|
187
|
+
group: 'parser',
|
|
188
|
+
task: 'normalize',
|
|
189
|
+
message: 'Finish token normalization',
|
|
190
|
+
timing: performance.now() - normalizeStart,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// 5. Resolve aliases and populate groups
|
|
194
|
+
for (const id in tokens) {
|
|
195
|
+
if (!Object.hasOwn(tokens, id)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const token = tokens[id];
|
|
199
|
+
applyAliases(token, { tokens, ast, node: token.sourceNode, logger });
|
|
200
|
+
token.mode['.'].$value = token.$value;
|
|
201
|
+
if (token.aliasOf) {
|
|
202
|
+
token.mode['.'].aliasOf = token.aliasOf;
|
|
203
|
+
}
|
|
204
|
+
if (token.partialAliasOf) {
|
|
205
|
+
token.mode['.'].partialAliasOf = token.partialAliasOf;
|
|
206
|
+
}
|
|
207
|
+
const { group: parentGroup } = splitID(id);
|
|
208
|
+
for (const siblingID in tokens) {
|
|
209
|
+
const { group: siblingGroup } = splitID(siblingID);
|
|
210
|
+
if (siblingGroup?.startsWith(parentGroup)) {
|
|
211
|
+
token.group.tokens.push(siblingID);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 6. resolve mode aliases
|
|
217
|
+
const modesStart = performance.now();
|
|
218
|
+
logger.debug({
|
|
219
|
+
group: 'parser',
|
|
220
|
+
task: 'modes',
|
|
221
|
+
message: 'Start mode resolution',
|
|
222
|
+
});
|
|
223
|
+
for (const id in tokens) {
|
|
224
|
+
if (!Object.hasOwn(tokens, id)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const mode in tokens[id].mode) {
|
|
228
|
+
if (mode === '.') {
|
|
229
|
+
continue; // skip shadow of root value
|
|
230
|
+
}
|
|
231
|
+
applyAliases(tokens[id].mode[mode], { tokens, ast, node: tokens[id].mode[mode].sourceNode, logger });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
logger.debug({
|
|
235
|
+
group: 'parser',
|
|
236
|
+
task: 'modes',
|
|
237
|
+
message: 'Finish token modes',
|
|
238
|
+
timing: performance.now() - modesStart,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
logger.debug({
|
|
242
|
+
group: 'parser',
|
|
243
|
+
task: 'core',
|
|
244
|
+
message: 'Finish all parser tasks',
|
|
245
|
+
timing: performance.now() - totalStart,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
tokens,
|
|
250
|
+
ast,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Determine if an input is likely a JSON string
|
|
256
|
+
* @param {string} input
|
|
257
|
+
* @return {boolean}
|
|
258
|
+
*/
|
|
259
|
+
export function maybeJSONString(input) {
|
|
260
|
+
return typeof input === 'string' && input.trim().startsWith('{');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve alias
|
|
265
|
+
* @param {string} alias
|
|
266
|
+
* @param {Object} options
|
|
267
|
+
* @param {Record<string, TokenNormalized>} options.tokens
|
|
268
|
+
* @param {Logger} options.logger
|
|
269
|
+
* @param {AnyNode | undefined} options.node
|
|
270
|
+
* @param {DocumentNode | undefined} options.ast
|
|
271
|
+
* @param {string[]=[]} options.scanned
|
|
272
|
+
* @param {string}
|
|
273
|
+
*/
|
|
274
|
+
export function resolveAlias(alias, { tokens, logger, ast, node, scanned = [] }) {
|
|
275
|
+
const { id } = parseAlias(alias);
|
|
276
|
+
if (!tokens[id]) {
|
|
277
|
+
logger.error({ message: `Alias "${alias}" not found`, ast, node });
|
|
278
|
+
}
|
|
279
|
+
if (scanned.includes(id)) {
|
|
280
|
+
logger.error({ message: `Circular alias detected from "${alias}"`, ast, node });
|
|
281
|
+
}
|
|
282
|
+
const token = tokens[id];
|
|
283
|
+
if (!isAlias(token.$value)) {
|
|
284
|
+
return id;
|
|
285
|
+
}
|
|
286
|
+
return resolveAlias(alias, { tokens, logger, ast, node, scanned: [...scanned, id] });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Resolve aliases, update values, and mutate `token` to add `aliasOf` / `partialAliasOf` */
|
|
290
|
+
function applyAliases(token, { tokens, logger, ast, node }) {
|
|
291
|
+
// handle simple aliases
|
|
292
|
+
if (isAlias(token.$value)) {
|
|
293
|
+
const aliasOfID = resolveAlias(token.$value, { tokens, logger, node, ast });
|
|
294
|
+
const { mode: aliasMode } = parseAlias(token.$value);
|
|
295
|
+
const aliasOf = tokens[aliasOfID];
|
|
296
|
+
token.aliasOf = aliasOfID;
|
|
297
|
+
token.$value = aliasOf.mode[aliasMode]?.$value || aliasOf.$value;
|
|
298
|
+
if (token.$type && token.$type !== aliasOf.$type) {
|
|
299
|
+
logger.warn({
|
|
300
|
+
message: `Token ${token.id} has $type "${token.$type}" but aliased ${aliasOfID} of $type "${aliasOf.$type}"`,
|
|
301
|
+
node,
|
|
302
|
+
ast,
|
|
303
|
+
});
|
|
304
|
+
token.$type = aliasOf.$type;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// handle aliases within array values (e.g. cubicBezier, gradient)
|
|
308
|
+
else if (Array.isArray(token.$value)) {
|
|
309
|
+
// some arrays are primitives, some are objects. handle both
|
|
310
|
+
for (let i = 0; i < token.$value.length; i++) {
|
|
311
|
+
if (isAlias(token.$value[i])) {
|
|
312
|
+
if (!token.partialAliasOf) {
|
|
313
|
+
token.partialAliasOf = [];
|
|
314
|
+
}
|
|
315
|
+
const aliasOfID = resolveAlias(token.$value[i], { tokens, logger, node, ast });
|
|
316
|
+
const { mode: aliasMode } = parseAlias(token.$value[i]);
|
|
317
|
+
token.partialAliasOf[i] = aliasOfID;
|
|
318
|
+
token.$value[i] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
|
|
319
|
+
} else if (typeof token.$value[i] === 'object') {
|
|
320
|
+
for (const property in token.$value[i]) {
|
|
321
|
+
if (isAlias(token.$value[i][property])) {
|
|
322
|
+
if (!token.partialAliasOf) {
|
|
323
|
+
token.partialAliasOf = [];
|
|
324
|
+
}
|
|
325
|
+
if (!token.partialAliasOf[i]) {
|
|
326
|
+
token.partialAliasOf[i] = {};
|
|
327
|
+
}
|
|
328
|
+
const aliasOfID = resolveAlias(token.$value[i][property], { tokens, logger, node, ast });
|
|
329
|
+
const { mode: aliasMode } = parseAlias(token.$value[i][property]);
|
|
330
|
+
token.$value[i][property] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
|
|
331
|
+
token.partialAliasOf[i][property] = aliasOfID;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// handle aliases within object (composite) values (e.g. border, typography, transition)
|
|
338
|
+
else if (typeof token.$value === 'object') {
|
|
339
|
+
for (const property in token.$value) {
|
|
340
|
+
if (!Object.hasOwn(token.$value, property)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (isAlias(token.$value[property])) {
|
|
345
|
+
if (!token.partialAliasOf) {
|
|
346
|
+
token.partialAliasOf = {};
|
|
347
|
+
}
|
|
348
|
+
const aliasOfID = resolveAlias(token.$value[property], { tokens, logger, node, ast });
|
|
349
|
+
const { mode: aliasMode } = parseAlias(token.$value[property]);
|
|
350
|
+
token.partialAliasOf[property] = aliasOfID;
|
|
351
|
+
token.$value[property] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
|
|
352
|
+
}
|
|
353
|
+
// strokeStyle has an array within an object
|
|
354
|
+
else if (Array.isArray(token.$value[property])) {
|
|
355
|
+
for (let i = 0; i < token.$value[property].length; i++) {
|
|
356
|
+
if (isAlias(token.$value[property][i])) {
|
|
357
|
+
const aliasOfID = resolveAlias(token.$value[property][i], { tokens, logger, node, ast });
|
|
358
|
+
if (!token.partialAliasOf) {
|
|
359
|
+
token.partialAliasOf = {};
|
|
360
|
+
}
|
|
361
|
+
if (!token.partialAliasOf[property]) {
|
|
362
|
+
token.partialAliasOf[property] = [];
|
|
363
|
+
}
|
|
364
|
+
const { mode: aliasMode } = parseAlias(token.$value[property][i]);
|
|
365
|
+
token.partialAliasOf[property][i] = aliasOfID;
|
|
366
|
+
token.$value[property][i] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
package/parse/json.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
declare const CHILD_KEYS: {
|
|
9
|
+
Document: readonly ['body'];
|
|
10
|
+
Object: readonly ['members'];
|
|
11
|
+
Member: readonly ['name', 'value'];
|
|
12
|
+
Element: readonly ['value'];
|
|
13
|
+
Array: readonly ['elements'];
|
|
14
|
+
String: readonly [];
|
|
15
|
+
Number: readonly [];
|
|
16
|
+
Boolean: readonly [];
|
|
17
|
+
Null: readonly [];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Determines if a given value is an AST node. */
|
|
21
|
+
export function isNode(value: unknown): boolean;
|
|
22
|
+
|
|
23
|
+
/** Get ObjectNode members as object */
|
|
24
|
+
export function getObjMembers(node: ObjectNode): Record<string | number, ValueNode | undefined>;
|
|
25
|
+
|
|
26
|
+
/** Inject members to ObjectNode and return a clone */
|
|
27
|
+
export function injectObjMembers(node: ObjectNode, members: MemberNode[]): ObjectNode;
|
|
28
|
+
|
|
29
|
+
/** Variation of Momoa’s traverse(), which keeps track of global path */
|
|
30
|
+
export function traverse(root: AnyNode, visitor: Visitor): void;
|
package/parse/json.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("@humanwhocodes/momoa").AnyNode} AnyNode
|
|
3
|
+
* @typedef {import("@humanwhocodes/momoa").ObjectNode} ObjectNode
|
|
4
|
+
* @typedef {import("@humanwhocodes/momoa").ValueNode} ValueNode
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const CHILD_KEYS = {
|
|
8
|
+
Document: ['body'],
|
|
9
|
+
Object: ['members'],
|
|
10
|
+
Member: ['name', 'value'],
|
|
11
|
+
Element: ['value'],
|
|
12
|
+
Array: ['elements'],
|
|
13
|
+
String: [],
|
|
14
|
+
Number: [],
|
|
15
|
+
Boolean: [],
|
|
16
|
+
Null: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Determines if a given value is an AST node. */
|
|
20
|
+
export function isNode(value) {
|
|
21
|
+
return value && typeof value === 'object' && 'type' in value && typeof value.type === 'string';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get ObjectNode members as object
|
|
26
|
+
* @param {ObjectNode} node
|
|
27
|
+
* @return {Record<string, ValueNode}
|
|
28
|
+
*/
|
|
29
|
+
export function getObjMembers(node) {
|
|
30
|
+
const members = {};
|
|
31
|
+
if (node.type !== 'Object') {
|
|
32
|
+
return members;
|
|
33
|
+
}
|
|
34
|
+
for (const m of node.members) {
|
|
35
|
+
if (m.name.type !== 'String' && m.name.type !== 'Number') {
|
|
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, members = []) {
|
|
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, 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, parent, path = []) {
|
|
69
|
+
const nextPath = [...path];
|
|
70
|
+
if (node.type === 'Member') {
|
|
71
|
+
nextPath.push(node.name.value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
visitor.enter?.(node, parent, nextPath);
|
|
75
|
+
|
|
76
|
+
for (const key of CHILD_KEYS[node.type] ?? []) {
|
|
77
|
+
const value = node[key];
|
|
78
|
+
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (let i = 0; i < value.length; i++) {
|
|
82
|
+
visitNode(value[i], node, key === 'elements' ? [...nextPath, String(i)] : nextPath);
|
|
83
|
+
}
|
|
84
|
+
} else if (isNode(value)) {
|
|
85
|
+
visitNode(value, node, nextPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
visitor.exit?.(node, parent, nextPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
visitNode(root, undefined, []);
|
|
94
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isAlias, parseColor } from '@terrazzo/token-tools';
|
|
2
|
+
|
|
3
|
+
export const FONT_WEIGHT_MAP = {
|
|
4
|
+
thin: 100,
|
|
5
|
+
hairline: 100,
|
|
6
|
+
'extra-light': 200,
|
|
7
|
+
'ultra-light': 200,
|
|
8
|
+
light: 300,
|
|
9
|
+
normal: 400,
|
|
10
|
+
regular: 400,
|
|
11
|
+
book: 400,
|
|
12
|
+
medium: 500,
|
|
13
|
+
'semi-bold': 600,
|
|
14
|
+
'demi-bold': 600,
|
|
15
|
+
bold: 700,
|
|
16
|
+
'extra-bold': 800,
|
|
17
|
+
'ultra-bold': 800,
|
|
18
|
+
black: 900,
|
|
19
|
+
heavy: 900,
|
|
20
|
+
'extra-black': 950,
|
|
21
|
+
'ultra-black': 950,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function normalizeValue(token) {
|
|
25
|
+
if (isAlias(token.$value)) {
|
|
26
|
+
return token.$value;
|
|
27
|
+
}
|
|
28
|
+
switch (token.$type) {
|
|
29
|
+
case 'boolean': {
|
|
30
|
+
return !!token.$value;
|
|
31
|
+
}
|
|
32
|
+
case 'border': {
|
|
33
|
+
return {
|
|
34
|
+
color: normalizeValue({ $type: 'color', $value: token.$value.color ?? '#000000' }),
|
|
35
|
+
style: normalizeValue({ $type: 'strokeStyle', $value: token.$value.style ?? 'solid' }),
|
|
36
|
+
width: normalizeValue({ $type: 'dimension', $value: token.$value.width }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
case 'color': {
|
|
40
|
+
return typeof token.$value === 'string' ? parseColor(token.$value) : token.$value;
|
|
41
|
+
}
|
|
42
|
+
case 'cubicBezier': {
|
|
43
|
+
return token.$value.map((value) =>
|
|
44
|
+
typeof value === 'number' ? normalizeValue({ $type: 'number', $value: value }) : value,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
case 'dimension': {
|
|
48
|
+
if (token.$value === 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return typeof token.$value === 'number' ? `${token.$value}px` : token.$value;
|
|
52
|
+
}
|
|
53
|
+
case 'duration': {
|
|
54
|
+
if (token.$value === 0) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
return typeof token.$value === 'number' ? `${token.$value}ms` : token.$value;
|
|
58
|
+
}
|
|
59
|
+
case 'fontFamily': {
|
|
60
|
+
return Array.isArray(token.$value) ? token.$value : [token.$value];
|
|
61
|
+
}
|
|
62
|
+
case 'fontWeight': {
|
|
63
|
+
if (typeof token.$value === 'string' && FONT_WEIGHT_MAP[token.$value]) {
|
|
64
|
+
return FONT_WEIGHT_MAP[token.$value];
|
|
65
|
+
}
|
|
66
|
+
return Number.parseInt(token.$value);
|
|
67
|
+
}
|
|
68
|
+
case 'gradient': {
|
|
69
|
+
const output = [];
|
|
70
|
+
for (let i = 0; i < token.$value.length; i++) {
|
|
71
|
+
const stop = { ...token.$value[i] };
|
|
72
|
+
stop.color = normalizeValue({ $type: 'color', $value: stop.color });
|
|
73
|
+
if (typeof stop.position !== 'number') {
|
|
74
|
+
stop.position = i / (token.$value.length - 1);
|
|
75
|
+
}
|
|
76
|
+
output.push(stop);
|
|
77
|
+
}
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
case 'number': {
|
|
81
|
+
return typeof token.$value === 'number' ? token.$value : Number.parseFloat(token.$value);
|
|
82
|
+
}
|
|
83
|
+
case 'shadow': {
|
|
84
|
+
return Array.isArray(token.$value) ? token.$value : [token.$value];
|
|
85
|
+
}
|
|
86
|
+
case 'strokeStyle': {
|
|
87
|
+
return token.$value;
|
|
88
|
+
}
|
|
89
|
+
case 'string': {
|
|
90
|
+
return String(token.$value);
|
|
91
|
+
}
|
|
92
|
+
case 'transition': {
|
|
93
|
+
return {
|
|
94
|
+
duration: normalizeValue({ $type: 'duration', $value: token.$value.duration ?? 0 }),
|
|
95
|
+
delay: normalizeValue({ $type: 'duration', $value: token.$value.delay ?? 0 }),
|
|
96
|
+
timingFunction: normalizeValue({ $type: 'cubicBezier', $value: token.$value.timingFunction }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case 'typography': {
|
|
100
|
+
const output = {};
|
|
101
|
+
for (const k in token.$value) {
|
|
102
|
+
if (k === 'fontSize') {
|
|
103
|
+
output[k] = normalizeValue({ $type: 'dimension', $value: token.$value[k] });
|
|
104
|
+
} else {
|
|
105
|
+
output[k] = token.$value[k];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
return token.$value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AnyNode, DocumentNode, MemberNode, ValueNode } from '@humanwhocodes/momoa';
|
|
2
|
+
import type Logger from '../logger.js';
|
|
3
|
+
|
|
4
|
+
declare const FONT_WEIGHT_VALUES: Set<string>;
|
|
5
|
+
|
|
6
|
+
declare const STROKE_STYLE_VALUES: Set<string>;
|
|
7
|
+
declare const STROKE_STYLE_LINE_CAP_VALUES: Set<string>;
|
|
8
|
+
|
|
9
|
+
export interface ValidateOptions {
|
|
10
|
+
ast: DocumentNode;
|
|
11
|
+
logger: Logger;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateAlias($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
15
|
+
|
|
16
|
+
export function validateBorder($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
17
|
+
|
|
18
|
+
export function validateColor($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
19
|
+
|
|
20
|
+
export function validateCubicBézier($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
21
|
+
|
|
22
|
+
export function validateDimension($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
23
|
+
|
|
24
|
+
export function validateDuration($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
25
|
+
|
|
26
|
+
export function validateFontFamily($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
27
|
+
|
|
28
|
+
export function validateFontWeight($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
29
|
+
|
|
30
|
+
export function validateGradient($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
31
|
+
|
|
32
|
+
export function validateNumber($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
33
|
+
|
|
34
|
+
export function validateShadowLayer($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
35
|
+
|
|
36
|
+
export function validateStrokeStyle($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
37
|
+
|
|
38
|
+
export function validateTransition($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
39
|
+
|
|
40
|
+
export function validateTypography($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
|
|
41
|
+
|
|
42
|
+
export default function validate(node: MemberNode, options: ValidateOptions): void;
|