@terrazzo/parser 0.0.11 → 0.0.13

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/parse/index.js CHANGED
@@ -3,56 +3,214 @@ import { isAlias, parseAlias, pluralize, splitID } from '@terrazzo/token-tools';
3
3
  import lintRunner from '../lint/index.js';
4
4
  import Logger from '../logger.js';
5
5
  import normalize from './normalize.js';
6
- import parseYAML from './yaml.js';
7
6
  import validate from './validate.js';
8
7
  import { getObjMembers, injectObjMembers, traverse } from './json.js';
9
8
 
10
9
  export * from './validate.js';
11
10
 
11
+ /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
12
+ /** @typedef {import("../config.js").Plugin} Plugin */
13
+ /** @typedef {import("../types.js").TokenNormalized} TokenNormalized */
12
14
  /**
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
15
  * @typedef {object} ParseResult
21
- * @typedef {Record<string, TokenNormalized} ParseResult.tokens
22
- * @typedef {DocumentNode} ParseResult.ast
16
+ * @property {Record<string, TokenNormalized} tokens
17
+ * @property {Object[]} src
18
+ */
19
+ /**
20
+ * @typedef {object} ParseInput
21
+ * @property {string | object} src
22
+ * @property {URL} [filename]
23
+ */
24
+ /**
25
+ * @typedef {object} ParseOptions
26
+ * @property {Logger} logger
27
+ * @property {import("../config.js").Config} config
28
+ * @property {import("yamlToMomoa")} yamlToMomoa
29
+ * @property {boolean} [skipLint=false]
30
+ * @property {boolean} [continueOnError=false]
23
31
  */
24
-
25
32
  /**
26
33
  * Parse
27
- * @param {string | object} input
28
- * @param {ParseOptions} options
34
+ * @param {ParseInput[]} input
35
+ * @param {ParseOptions} [options]
29
36
  * @return {Promise<ParseResult>}
30
37
  */
31
38
  export default async function parse(
32
39
  input,
33
- { logger = new Logger(), skipLint = false, config, continueOnError = false } = {},
40
+ { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa } = {},
34
41
  ) {
35
- const { plugins } = config;
42
+ let tokens = {};
43
+ // note: only keeps track of sources with locations on disk; in-memory sources are discarded
44
+ // (it’s only for reporting line numbers, which doesn’t mean as much for dynamic sources)
45
+ const sources = {};
46
+
47
+ if (!Array.isArray(input)) {
48
+ logger.error({ group: 'parser', task: 'init', message: 'Input must be an array of input objects.' });
49
+ }
50
+ for (let i = 0; i < input.length; i++) {
51
+ if (!input[i] || typeof input[i] !== 'object') {
52
+ logger.error({ group: 'parser', task: 'init', message: `Input (${i}) must be an object.` });
53
+ }
54
+ if (!input[i].src || (typeof input[i].src !== 'string' && typeof input[i].src !== 'object')) {
55
+ logger.error({
56
+ group: 'parser',
57
+ task: 'init',
58
+ message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
59
+ });
60
+ }
61
+ if (input[i].filename && !(input[i].filename instanceof URL)) {
62
+ logger.error({
63
+ group: 'parser',
64
+ task: 'init',
65
+ message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
66
+ });
67
+ }
68
+
69
+ const result = await parseSingle(input[i].src, {
70
+ filename: input[i].filename,
71
+ logger,
72
+ config,
73
+ skipLint,
74
+ continueOnError,
75
+ yamlToMomoa,
76
+ });
77
+
78
+ tokens = Object.assign(tokens, result.tokens);
79
+ if (input[i].filename) {
80
+ sources[input[i].filename.protocol === 'file:' ? input[i].filename.href : input[i].filename.href] = {
81
+ filename: input[i].filename,
82
+ src: result.src,
83
+ document: result.document,
84
+ };
85
+ }
86
+ }
36
87
 
37
88
  const totalStart = performance.now();
38
89
 
90
+ // 5. Resolve aliases and populate groups
91
+ for (const id in tokens) {
92
+ if (!Object.hasOwn(tokens, id)) {
93
+ continue;
94
+ }
95
+ const token = tokens[id];
96
+ applyAliases(token, {
97
+ tokens,
98
+ filename: sources[token.source.loc]?.filename,
99
+ src: sources[token.source.loc]?.src,
100
+ node: token.source.node,
101
+ logger,
102
+ });
103
+ token.mode['.'].$value = token.$value;
104
+ if (token.aliasOf) {
105
+ token.mode['.'].aliasOf = token.aliasOf;
106
+ }
107
+ if (token.partialAliasOf) {
108
+ token.mode['.'].partialAliasOf = token.partialAliasOf;
109
+ }
110
+ const { group: parentGroup } = splitID(id);
111
+ for (const siblingID in tokens) {
112
+ const { group: siblingGroup } = splitID(siblingID);
113
+ if (siblingGroup?.startsWith(parentGroup)) {
114
+ token.group.tokens.push(siblingID);
115
+ }
116
+ }
117
+ }
118
+
119
+ // 6. resolve mode aliases
120
+ const modesStart = performance.now();
121
+ logger.debug({
122
+ group: 'parser',
123
+ task: 'modes',
124
+ message: 'Start mode resolution',
125
+ });
126
+ for (const id in tokens) {
127
+ if (!Object.hasOwn(tokens, id)) {
128
+ continue;
129
+ }
130
+ for (const mode in tokens[id].mode) {
131
+ if (mode === '.') {
132
+ continue; // skip shadow of root value
133
+ }
134
+ applyAliases(tokens[id].mode[mode], { tokens, node: tokens[id].mode[mode].source.node, logger });
135
+ }
136
+ }
137
+ logger.debug({
138
+ group: 'parser',
139
+ task: 'modes',
140
+ message: 'Finish token modes',
141
+ timing: performance.now() - modesStart,
142
+ });
143
+
144
+ logger.debug({
145
+ group: 'parser',
146
+ task: 'core',
147
+ message: 'Finish all parser tasks',
148
+ timing: performance.now() - totalStart,
149
+ });
150
+
151
+ if (continueOnError) {
152
+ const { errorCount } = logger.stats();
153
+ if (errorCount > 0) {
154
+ logger.error({
155
+ message: `Parser encountered ${errorCount} ${pluralize(errorCount, 'error', 'errors')}. Exiting.`,
156
+ });
157
+ }
158
+ }
159
+
160
+ return {
161
+ tokens,
162
+ sources,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Parse a single input
168
+ * @param {string | object} input
169
+ * @param {object} options
170
+ * @param {URL} [options.filename]
171
+ * @param {Logger} [options.logger]
172
+ * @param {import("../config.js").Config} [options.config]
173
+ * @param {boolean} [options.skipLint]
174
+ */
175
+ async function parseSingle(input, { filename, logger, config, skipLint, continueOnError = false, yamlToMomoa }) {
39
176
  // 1. Build AST
40
- let source;
177
+ let src;
41
178
  if (typeof input === 'string') {
42
- source = input;
179
+ src = input;
43
180
  }
44
181
  const startParsing = performance.now();
45
182
  logger.debug({ group: 'parser', task: 'parse', message: 'Start tokens parsing' });
46
- let ast;
183
+ let document;
47
184
  if (typeof input === 'string' && !maybeJSONString(input)) {
48
- ast = parseYAML(input, { logger }); // if string, but not JSON, attempt YAML
185
+ if (yamlToMomoa) {
186
+ try {
187
+ document = yamlToMomoa(input); // if string, but not JSON, attempt YAML
188
+ } catch (err) {
189
+ logger.error({ message: String(err), filename, src: input, continueOnError });
190
+ }
191
+ } else {
192
+ logger.error({
193
+ group: 'parser',
194
+ task: 'parse',
195
+ message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
196
+
197
+ import { parse } from '@terrazzo/parser';
198
+ import yamlToMomoa from 'yaml-to-momoa';
199
+
200
+ parse(yamlString, { yamlToMomoa });`,
201
+ continueOnError: false, // fail here; no point in continuing
202
+ });
203
+ }
49
204
  } else {
50
- ast = parseJSON(typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), {
51
- mode: 'jsonc',
52
- }); // everything else: assert it’s JSON-serializable
205
+ document = parseJSON(
206
+ typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), // everything else: assert it’s JSON-serializable
207
+ {
208
+ mode: 'jsonc',
209
+ },
210
+ );
53
211
  }
54
- if (!source) {
55
- source = print(ast, { indent: 2 });
212
+ if (!src) {
213
+ src = print(document, { indent: 2 });
56
214
  }
57
215
  logger.debug({
58
216
  group: 'parser',
@@ -67,7 +225,7 @@ export default async function parse(
67
225
  const startValidation = performance.now();
68
226
  logger.debug({ group: 'parser', task: 'validate', message: 'Start tokens validation' });
69
227
  const $typeInheritance = {};
70
- traverse(ast, {
228
+ traverse(document, {
71
229
  enter(node, parent, path) {
72
230
  if (node.type === 'Member' && node.value.type === 'Object' && node.value.members) {
73
231
  const members = getObjMembers(node.value);
@@ -96,7 +254,8 @@ export default async function parse(
96
254
  if (parent$type && !members.$type) {
97
255
  sourceNode.value = injectObjMembers(sourceNode.value, [parent$type]);
98
256
  }
99
- validate(sourceNode, { source, logger });
257
+
258
+ validate(sourceNode, { filename, src, logger });
100
259
 
101
260
  const group = { id: splitID(id).group, tokens: [] };
102
261
  if (parent$type) {
@@ -117,7 +276,10 @@ export default async function parse(
117
276
  mode: {},
118
277
  originalValue: evaluate(node.value),
119
278
  group,
120
- sourceNode: sourceNode.value,
279
+ source: {
280
+ loc: filename ? filename.href : undefined,
281
+ node: sourceNode.value,
282
+ },
121
283
  };
122
284
  if (members.$description?.value) {
123
285
  token.$description = members.$description.value;
@@ -131,7 +293,10 @@ export default async function parse(
131
293
  id: token.id,
132
294
  $type: token.$type,
133
295
  $value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
134
- sourceNode: mode === '.' ? structuredClone(token.sourceNode) : modeValues[mode],
296
+ source: {
297
+ loc: filename ? filename.href : undefined,
298
+ node: mode === '.' ? structuredClone(token.source.node) : modeValues[mode],
299
+ },
135
300
  };
136
301
  if (token.$description) {
137
302
  token.mode[mode].$description = token.$description;
@@ -140,7 +305,7 @@ export default async function parse(
140
305
 
141
306
  tokens[id] = token;
142
307
  } else if (members.value) {
143
- logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, node, source });
308
+ logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
144
309
  }
145
310
  }
146
311
 
@@ -161,14 +326,14 @@ export default async function parse(
161
326
  });
162
327
 
163
328
  // 3. Execute lint runner with loaded plugins
164
- if (!skipLint && plugins?.length) {
329
+ if (!skipLint && config?.plugins?.length) {
165
330
  const lintStart = performance.now();
166
331
  logger.debug({
167
332
  group: 'parser',
168
333
  task: 'validate',
169
334
  message: 'Start token linting',
170
335
  });
171
- await lintRunner({ ast, source, config, logger });
336
+ await lintRunner({ document, filename, src, config, logger });
172
337
  logger.debug({
173
338
  group: 'parser',
174
339
  task: 'validate',
@@ -191,12 +356,12 @@ export default async function parse(
191
356
  try {
192
357
  tokens[id].$value = normalize(tokens[id]);
193
358
  } catch (err) {
194
- let node = tokens[id].sourceNode;
359
+ let { node } = tokens[id].source;
195
360
  const members = getObjMembers(node);
196
361
  if (members.$value) {
197
362
  node = members.$value;
198
363
  }
199
- logger.error({ message: err.message, source, node, continueOnError });
364
+ logger.error({ message: err.message, filename, src, node, continueOnError });
200
365
  }
201
366
  for (const mode in tokens[id].mode) {
202
367
  if (mode === '.') {
@@ -205,12 +370,18 @@ export default async function parse(
205
370
  try {
206
371
  tokens[id].mode[mode].$value = normalize(tokens[id].mode[mode]);
207
372
  } catch (err) {
208
- let node = tokens[id].sourceNode;
373
+ let { node } = tokens[id].source;
209
374
  const members = getObjMembers(node);
210
375
  if (members.$value) {
211
376
  node = members.$value;
212
377
  }
213
- logger.error({ message: err.message, source, node: tokens[id].mode[mode].sourceNode, continueOnError });
378
+ logger.error({
379
+ message: err.message,
380
+ filename,
381
+ src,
382
+ node: tokens[id].mode[mode].source.node,
383
+ continueOnError,
384
+ });
214
385
  }
215
386
  }
216
387
  }
@@ -221,74 +392,7 @@ export default async function parse(
221
392
  timing: performance.now() - normalizeStart,
222
393
  });
223
394
 
224
- // 5. Resolve aliases and populate groups
225
- for (const id in tokens) {
226
- if (!Object.hasOwn(tokens, id)) {
227
- continue;
228
- }
229
- const token = tokens[id];
230
- applyAliases(token, { tokens, source, node: token.sourceNode, logger });
231
- token.mode['.'].$value = token.$value;
232
- if (token.aliasOf) {
233
- token.mode['.'].aliasOf = token.aliasOf;
234
- }
235
- if (token.partialAliasOf) {
236
- token.mode['.'].partialAliasOf = token.partialAliasOf;
237
- }
238
- const { group: parentGroup } = splitID(id);
239
- for (const siblingID in tokens) {
240
- const { group: siblingGroup } = splitID(siblingID);
241
- if (siblingGroup?.startsWith(parentGroup)) {
242
- token.group.tokens.push(siblingID);
243
- }
244
- }
245
- }
246
-
247
- // 6. resolve mode aliases
248
- const modesStart = performance.now();
249
- logger.debug({
250
- group: 'parser',
251
- task: 'modes',
252
- message: 'Start mode resolution',
253
- });
254
- for (const id in tokens) {
255
- if (!Object.hasOwn(tokens, id)) {
256
- continue;
257
- }
258
- for (const mode in tokens[id].mode) {
259
- if (mode === '.') {
260
- continue; // skip shadow of root value
261
- }
262
- applyAliases(tokens[id].mode[mode], { tokens, source, node: tokens[id].mode[mode].sourceNode, logger });
263
- }
264
- }
265
- logger.debug({
266
- group: 'parser',
267
- task: 'modes',
268
- message: 'Finish token modes',
269
- timing: performance.now() - modesStart,
270
- });
271
-
272
- logger.debug({
273
- group: 'parser',
274
- task: 'core',
275
- message: 'Finish all parser tasks',
276
- timing: performance.now() - totalStart,
277
- });
278
-
279
- if (continueOnError) {
280
- const { errorCount } = logger.stats();
281
- if (errorCount > 0) {
282
- logger.error({
283
- message: `Parser encountered ${errorCount} ${pluralize(errorCount, 'error', 'errors')}. Exiting.`,
284
- });
285
- }
286
- }
287
-
288
- return {
289
- tokens,
290
- ast,
291
- };
395
+ return { tokens, document, src };
292
396
  }
293
397
 
294
398
  /**
@@ -306,31 +410,32 @@ export function maybeJSONString(input) {
306
410
  * @param {Object} options
307
411
  * @param {Record<string, TokenNormalized>} options.tokens
308
412
  * @param {Logger} options.logger
413
+ * @param {string} [options.filename]
309
414
  * @param {AnyNode} [options.node]
310
415
  * @param {string} [options.string]
311
- * @param {string[]=[]} options.scanned
416
+ * @param {string} [options.scanned=[]]
312
417
  * @param {string}
313
418
  */
314
- export function resolveAlias(alias, { tokens, logger, source, node, scanned = [] }) {
419
+ export function resolveAlias(alias, { tokens, logger, filename, src, node, scanned = [] }) {
315
420
  const { id } = parseAlias(alias);
316
421
  if (!tokens[id]) {
317
- logger.error({ message: `Alias "${alias}" not found`, source, node });
422
+ logger.error({ message: `Alias "${alias}" not found`, filename, src, node });
318
423
  }
319
424
  if (scanned.includes(id)) {
320
- logger.error({ message: `Circular alias detected from "${alias}"`, source, node });
425
+ logger.error({ message: `Circular alias detected from "${alias}"`, filename, src, node });
321
426
  }
322
427
  const token = tokens[id];
323
428
  if (!isAlias(token.$value)) {
324
429
  return id;
325
430
  }
326
- return resolveAlias(token.$value, { tokens, logger, node, source, scanned: [...scanned, id] });
431
+ return resolveAlias(token.$value, { tokens, logger, filename, node, src, scanned: [...scanned, id] });
327
432
  }
328
433
 
329
434
  /** Resolve aliases, update values, and mutate `token` to add `aliasOf` / `partialAliasOf` */
330
- function applyAliases(token, { tokens, logger, source, node }) {
435
+ function applyAliases(token, { tokens, logger, filename, src, node }) {
331
436
  // handle simple aliases
332
437
  if (isAlias(token.$value)) {
333
- const aliasOfID = resolveAlias(token.$value, { tokens, logger, node, source });
438
+ const aliasOfID = resolveAlias(token.$value, { tokens, logger, filename, node, src });
334
439
  const { mode: aliasMode } = parseAlias(token.$value);
335
440
  const aliasOf = tokens[aliasOfID];
336
441
  token.aliasOf = aliasOfID;
@@ -353,7 +458,7 @@ function applyAliases(token, { tokens, logger, source, node }) {
353
458
  if (!token.partialAliasOf) {
354
459
  token.partialAliasOf = [];
355
460
  }
356
- const aliasOfID = resolveAlias(token.$value[i], { tokens, logger, node, source });
461
+ const aliasOfID = resolveAlias(token.$value[i], { tokens, logger, filename, node, src });
357
462
  const { mode: aliasMode } = parseAlias(token.$value[i]);
358
463
  token.partialAliasOf[i] = aliasOfID;
359
464
  token.$value[i] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
@@ -366,7 +471,7 @@ function applyAliases(token, { tokens, logger, source, node }) {
366
471
  if (!token.partialAliasOf[i]) {
367
472
  token.partialAliasOf[i] = {};
368
473
  }
369
- const aliasOfID = resolveAlias(token.$value[i][property], { tokens, logger, node, source });
474
+ const aliasOfID = resolveAlias(token.$value[i][property], { tokens, logger, filename, node, src });
370
475
  const { mode: aliasMode } = parseAlias(token.$value[i][property]);
371
476
  token.$value[i][property] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
372
477
  token.partialAliasOf[i][property] = aliasOfID;
@@ -386,7 +491,7 @@ function applyAliases(token, { tokens, logger, source, node }) {
386
491
  if (!token.partialAliasOf) {
387
492
  token.partialAliasOf = {};
388
493
  }
389
- const aliasOfID = resolveAlias(token.$value[property], { tokens, logger, node, source });
494
+ const aliasOfID = resolveAlias(token.$value[property], { tokens, logger, filename, node, src });
390
495
  const { mode: aliasMode } = parseAlias(token.$value[property]);
391
496
  token.partialAliasOf[property] = aliasOfID;
392
497
  token.$value[property] = tokens[aliasOfID].mode[aliasMode]?.$value || tokens[aliasOfID].$value;
@@ -395,7 +500,7 @@ function applyAliases(token, { tokens, logger, source, node }) {
395
500
  else if (Array.isArray(token.$value[property])) {
396
501
  for (let i = 0; i < token.$value[property].length; i++) {
397
502
  if (isAlias(token.$value[property][i])) {
398
- const aliasOfID = resolveAlias(token.$value[property][i], { tokens, logger, node, source });
503
+ const aliasOfID = resolveAlias(token.$value[property][i], { tokens, logger, filename, node, src });
399
504
  if (!token.partialAliasOf) {
400
505
  token.partialAliasOf = {};
401
506
  }
@@ -7,7 +7,8 @@ declare const STROKE_STYLE_VALUES: Set<string>;
7
7
  declare const STROKE_STYLE_LINE_CAP_VALUES: Set<string>;
8
8
 
9
9
  export interface ValidateOptions {
10
- source: string;
10
+ filename?: URL;
11
+ src: string;
11
12
  logger: Logger;
12
13
  }
13
14