@terrazzo/parser 0.0.12 → 0.0.14

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @terrazzo/parser
2
2
 
3
+ ## 0.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - [#289](https://github.com/terrazzoapp/terrazzo/pull/289) [`0fc9738`](https://github.com/terrazzoapp/terrazzo/commit/0fc9738bb3dfecb680d225e4bd3970f21cfe8079) Thanks [@drwpow](https://github.com/drwpow)! - Add YAML support
8
+
9
+ - [#291](https://github.com/terrazzoapp/terrazzo/pull/291) [`6a875b1`](https://github.com/terrazzoapp/terrazzo/commit/6a875b163539dba8111911851a7819732056b3aa) Thanks [@drwpow](https://github.com/drwpow)! - Allow negative dimension values
10
+
3
11
  ## 0.0.12
4
12
 
5
13
  ### Patch Changes
package/config.js CHANGED
@@ -65,8 +65,10 @@ export default function defineConfig(rawConfig, { logger = new Logger(), cwd } =
65
65
  } else if (typeof config.outDir !== 'string') {
66
66
  logger.error({ label: 'config.outDir', message: `Expected string, received ${JSON.stringify(config.outDir)}` });
67
67
  } else {
68
- // note: always add trailing slash so URL treats it as a directory
69
- config.outDir = new URL(config.outDir.replace(TRAILING_SLASH_RE, '/'), cwd);
68
+ config.outDir = new URL(config.outDir, cwd);
69
+ // always add trailing slash so URL treats it as a directory.
70
+ // do AFTER it has been normalized to POSIX paths with `href` (don’t use Node internals here! This may run in the browser)
71
+ config.outDir = new URL(config.outDir.href.replace(TRAILING_SLASH_RE, '/'));
70
72
  }
71
73
 
72
74
  // config.plugins
@@ -0,0 +1,56 @@
1
+ // MIT License
2
+ //
3
+ // Copyright (c) 2014-present Sebastian McKenzie and other contributors
4
+ //
5
+ // Permission is hereby granted, free of charge, to any person obtaining
6
+ // a copy of this software and associated documentation files (the
7
+ // "Software"), to deal in the Software without restriction, including
8
+ // without limitation the rights to use, copy, modify, merge, publish,
9
+ // distribute, sublicense, and/or sell copies of the Software, and to
10
+ // permit persons to whom the Software is furnished to do so, subject to
11
+ // the following conditions:
12
+ //
13
+ // The above copyright notice and this permission notice shall be
14
+ // included in all copies or substantial portions of the Software.
15
+ //
16
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ export interface Location {
25
+ line: number;
26
+ column: number;
27
+ }
28
+
29
+ export interface NodeLocation {
30
+ end?: Location;
31
+ start: Location;
32
+ }
33
+
34
+ export interface Options {
35
+ /** Syntax highlight the code as JavaScript for terminals. default: false */
36
+ highlightCode?: boolean;
37
+ /** The number of lines to show above the error. default: 2 */
38
+ linesAbove?: number;
39
+ /** The number of lines to show below the error. default: 3 */
40
+ linesBelow?: number;
41
+ /**
42
+ * Forcibly syntax highlight the code as JavaScript (for non-terminals);
43
+ * overrides highlightCode.
44
+ * default: false
45
+ */
46
+ forceColor?: boolean;
47
+ /**
48
+ * Pass in a string to be displayed inline (if possible) next to the
49
+ * highlighted location in the code. If it can't be positioned inline,
50
+ * it will be placed above the code frame.
51
+ * default: nothing
52
+ */
53
+ message?: string;
54
+ }
55
+
56
+ export function codeFrameColumns(input: string, location: NodeLocation, options?: Options): string;
@@ -0,0 +1,141 @@
1
+ // This is copied from @babel/code-frame package but without the heavyweight color highlighting
2
+ // (note: Babel loads both chalk AND picocolors, and doesn’t treeshake well)
3
+ // Babel is MIT-licensed and unaffiliated with this project.
4
+
5
+ // MIT License
6
+ //
7
+ // Copyright (c) 2014-present Sebastian McKenzie and other contributors
8
+ //
9
+ // Permission is hereby granted, free of charge, to any person obtaining
10
+ // a copy of this software and associated documentation files (the
11
+ // "Software"), to deal in the Software without restriction, including
12
+ // without limitation the rights to use, copy, modify, merge, publish,
13
+ // distribute, sublicense, and/or sell copies of the Software, and to
14
+ // permit persons to whom the Software is furnished to do so, subject to
15
+ // the following conditions:
16
+ //
17
+ // The above copyright notice and this permission notice shall be
18
+ // included in all copies or substantial portions of the Software.
19
+ //
20
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ /**
29
+ * Extract what lines should be marked and highlighted.
30
+ */
31
+
32
+ function getMarkerLines(loc, source, opts = {}) {
33
+ const startLoc = {
34
+ column: 0,
35
+ line: -1,
36
+ ...loc.start,
37
+ };
38
+ const endLoc = {
39
+ ...startLoc,
40
+ ...loc.end,
41
+ };
42
+ const { linesAbove = 2, linesBelow = 3 } = opts || {};
43
+ const startLine = startLoc.line;
44
+ const startColumn = startLoc.column;
45
+ const endLine = endLoc.line;
46
+ const endColumn = endLoc.column;
47
+
48
+ let start = Math.max(startLine - (linesAbove + 1), 0);
49
+ let end = Math.min(source.length, endLine + linesBelow);
50
+
51
+ if (startLine === -1) {
52
+ start = 0;
53
+ }
54
+
55
+ if (endLine === -1) {
56
+ end = source.length;
57
+ }
58
+
59
+ const lineDiff = endLine - startLine;
60
+ const markerLines = {};
61
+
62
+ if (lineDiff) {
63
+ for (let i = 0; i <= lineDiff; i++) {
64
+ const lineNumber = i + startLine;
65
+
66
+ if (!startColumn) {
67
+ markerLines[lineNumber] = true;
68
+ } else if (i === 0) {
69
+ const sourceLength = source[lineNumber - 1].length;
70
+
71
+ markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
72
+ } else if (i === lineDiff) {
73
+ markerLines[lineNumber] = [0, endColumn];
74
+ } else {
75
+ const sourceLength = source[lineNumber - i].length;
76
+
77
+ markerLines[lineNumber] = [0, sourceLength];
78
+ }
79
+ }
80
+ } else {
81
+ if (startColumn === endColumn) {
82
+ if (startColumn) {
83
+ markerLines[startLine] = [startColumn, 0];
84
+ } else {
85
+ markerLines[startLine] = true;
86
+ }
87
+ } else {
88
+ markerLines[startLine] = [startColumn, endColumn - startColumn];
89
+ }
90
+ }
91
+
92
+ return { start, end, markerLines };
93
+ }
94
+
95
+ /**
96
+ * RegExp to test for newlines in terminal.
97
+ */
98
+
99
+ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
100
+
101
+ export function codeFrameColumns(rawLines, loc, opts = {}) {
102
+ const lines = rawLines.split(NEWLINE);
103
+ const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
104
+ const hasColumns = loc.start && typeof loc.start.column === 'number';
105
+
106
+ const numberMaxWidth = String(end).length;
107
+
108
+ let frame = rawLines
109
+ .split(NEWLINE, end)
110
+ .slice(start, end)
111
+ .map((line, index) => {
112
+ const number = start + 1 + index;
113
+ const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
114
+ const gutter = ` ${paddedNumber} |`;
115
+ const hasMarker = markerLines[number];
116
+ const lastMarkerLine = !markerLines[number + 1];
117
+ if (hasMarker) {
118
+ let markerLine = '';
119
+ if (Array.isArray(hasMarker)) {
120
+ const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, ' ');
121
+ const numberOfMarkers = hasMarker[1] || 1;
122
+
123
+ markerLine = ['\n ', gutter.replace(/\d/g, ' '), ' ', markerSpacing, '^'.repeat(numberOfMarkers)].join('');
124
+
125
+ if (lastMarkerLine && opts.message) {
126
+ markerLine += ` ${opts.message}`;
127
+ }
128
+ }
129
+ return ['>', gutter, line.length > 0 ? ` ${line}` : '', markerLine].join('');
130
+ } else {
131
+ return ` ${gutter}${line.length > 0 ? ` ${line}` : ''}`;
132
+ }
133
+ })
134
+ .join('\n');
135
+
136
+ if (opts.message && !hasColumns) {
137
+ frame = `${' '.repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
138
+ }
139
+
140
+ return frame;
141
+ }
@@ -1,5 +1,4 @@
1
1
  import { isAlias, isTokenMatch } from '@terrazzo/token-tools';
2
- import deepEqual from 'deep-equal';
3
2
 
4
3
  export default function ruleDuplicateValues({ tokens, rule: { severity }, options }) {
5
4
  if (severity === 'off') {
@@ -51,7 +50,7 @@ export default function ruleDuplicateValues({ tokens, rule: { severity }, option
51
50
  // everything else: use deepEqual
52
51
  let isDuplicate = false;
53
52
  for (const v of values[t.$type]?.values() ?? []) {
54
- if (deepEqual(t.$value, v)) {
53
+ if (JSON.stringify(t.$value) === JSON.stringify(v)) {
55
54
  notices.push({ message: `Duplicated value (${t.id})`, node: t.sourceNode });
56
55
  isDuplicate = true;
57
56
  break;
package/logger.js CHANGED
@@ -1,7 +1,6 @@
1
- import { codeFrameColumns } from '@babel/code-frame';
2
1
  import color from 'picocolors';
3
- import { fileURLToPath } from 'node:url';
4
2
  import wcmatch from 'wildcard-match';
3
+ import { codeFrameColumns } from './lib/code-frame.js';
5
4
 
6
5
  export const LOG_ORDER = ['error', 'warn', 'info', 'debug'];
7
6
 
@@ -26,7 +25,8 @@ export function formatMessage(entry, severity) {
26
25
  }
27
26
  if (entry.src) {
28
27
  const start = entry.node?.loc?.start;
29
- message = `${message}\n\n${entry.filename ? `${fileURLToPath(entry.filename)}:${start?.line ?? 0}:${start?.column ?? 0}\n\n` : ''}${codeFrameColumns(entry.src, { start })}`;
28
+ // note: strip "file://" protocol, but not href
29
+ message = `${message}\n\n${entry.filename ? `${entry.filename.href.replace(/^file:\/\//, '')}:${start?.line ?? 0}:${start?.column ?? 0}\n\n` : ''}${codeFrameColumns(entry.src, { start }, { highlightCode: false })}`;
30
30
  }
31
31
  return message;
32
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/parser",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
5
5
  "type": "module",
6
6
  "author": {
@@ -30,19 +30,20 @@
30
30
  },
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@babel/code-frame": "^7.24.7",
34
- "@humanwhocodes/momoa": "^3.1.1",
33
+ "@humanwhocodes/momoa": "^3.2.0",
35
34
  "@types/babel__code-frame": "^7.0.6",
36
35
  "@types/culori": "^2.1.1",
37
- "@types/deep-equal": "^1.0.4",
38
36
  "culori": "^4.0.1",
39
- "deep-equal": "^2.2.3",
40
37
  "is-what": "^4.1.16",
41
38
  "merge-anything": "^5.1.7",
42
39
  "picocolors": "^1.0.1",
43
40
  "wildcard-match": "^5.1.3",
44
- "yaml": "^2.4.5",
45
- "@terrazzo/token-tools": "^0.0.6"
41
+ "yaml": "^2.5.0",
42
+ "@terrazzo/token-tools": "^0.0.7"
43
+ },
44
+ "devDependencies": {
45
+ "esbuild": "^0.23.0",
46
+ "yaml-to-momoa": "^0.0.1"
46
47
  },
47
48
  "scripts": {
48
49
  "lint": "biome check .",
package/parse/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { DocumentNode } from '@humanwhocodes/momoa';
2
2
  import type { TokenNormalized } from '@terrazzo/token-tools';
3
+ import type yamlToMomoa from 'yaml-to-momoa';
3
4
  import type { ConfigInit } from '../config.js';
4
5
  import type Logger from '../logger.js';
5
6
 
@@ -19,6 +20,8 @@ export interface ParseOptions {
19
20
  skipLint?: boolean;
20
21
  /** Continue on error? (Useful for `tz check`) (default: false) */
21
22
  continueOnError?: boolean;
23
+ /** Provide yamlToMomoa module to parse YAML (by default, this isn’t shipped to cut down on package weight) */
24
+ yamlToMomoa?: typeof yamlToMomoa;
22
25
  }
23
26
 
24
27
  export interface ParseResult {
package/parse/index.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { evaluate, parse as parseJSON, print } from '@humanwhocodes/momoa';
2
2
  import { isAlias, parseAlias, pluralize, splitID } from '@terrazzo/token-tools';
3
- import { fileURLToPath } from 'node:url';
4
3
  import lintRunner from '../lint/index.js';
5
4
  import Logger from '../logger.js';
6
5
  import normalize from './normalize.js';
7
- import parseYAML from './yaml.js';
8
6
  import validate from './validate.js';
9
7
  import { getObjMembers, injectObjMembers, traverse } from './json.js';
10
8
 
@@ -27,6 +25,7 @@ export * from './validate.js';
27
25
  * @typedef {object} ParseOptions
28
26
  * @property {Logger} logger
29
27
  * @property {import("../config.js").Config} config
28
+ * @property {import("yamlToMomoa")} yamlToMomoa
30
29
  * @property {boolean} [skipLint=false]
31
30
  * @property {boolean} [continueOnError=false]
32
31
  */
@@ -38,7 +37,7 @@ export * from './validate.js';
38
37
  */
39
38
  export default async function parse(
40
39
  input,
41
- { logger = new Logger(), skipLint = false, config = {}, continueOnError = false } = {},
40
+ { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa } = {},
42
41
  ) {
43
42
  let tokens = {};
44
43
  // note: only keeps track of sources with locations on disk; in-memory sources are discarded
@@ -73,11 +72,12 @@ export default async function parse(
73
72
  config,
74
73
  skipLint,
75
74
  continueOnError,
75
+ yamlToMomoa,
76
76
  });
77
77
 
78
78
  tokens = Object.assign(tokens, result.tokens);
79
79
  if (input[i].filename) {
80
- sources[input[i].filename.protocol === 'file:' ? fileURLToPath(input[i].filename) : input[i].filename.href] = {
80
+ sources[input[i].filename.protocol === 'file:' ? input[i].filename.href : input[i].filename.href] = {
81
81
  filename: input[i].filename,
82
82
  src: result.src,
83
83
  document: result.document,
@@ -172,7 +172,7 @@ export default async function parse(
172
172
  * @param {import("../config.js").Config} [options.config]
173
173
  * @param {boolean} [options.skipLint]
174
174
  */
175
- async function parseSingle(input, { filename, logger, config, skipLint, continueOnError = false }) {
175
+ async function parseSingle(input, { filename, logger, config, skipLint, continueOnError = false, yamlToMomoa }) {
176
176
  // 1. Build AST
177
177
  let src;
178
178
  if (typeof input === 'string') {
@@ -182,7 +182,25 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
182
182
  logger.debug({ group: 'parser', task: 'parse', message: 'Start tokens parsing' });
183
183
  let document;
184
184
  if (typeof input === 'string' && !maybeJSONString(input)) {
185
- document = 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
+ }
186
204
  } else {
187
205
  document = parseJSON(
188
206
  typeof input === 'string' ? input : JSON.stringify(input, undefined, 2), // everything else: assert it’s JSON-serializable
@@ -259,7 +277,7 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
259
277
  originalValue: evaluate(node.value),
260
278
  group,
261
279
  source: {
262
- loc: filename ? fileURLToPath(filename) : undefined,
280
+ loc: filename ? filename.href : undefined,
263
281
  node: sourceNode.value,
264
282
  },
265
283
  };
@@ -276,7 +294,7 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
276
294
  $type: token.$type,
277
295
  $value: mode === '.' ? token.$value : evaluate(modeValues[mode]),
278
296
  source: {
279
- loc: filename ? fileURLToPath(filename) : undefined,
297
+ loc: filename ? filename.href : undefined,
280
298
  node: mode === '.' ? structuredClone(token.source.node) : modeValues[mode],
281
299
  },
282
300
  };
package/parse/validate.js CHANGED
@@ -266,9 +266,9 @@ export function validateDimension($value, node, { filename, src, logger }) {
266
266
  logger.error({ message: `Expected string, received ${$value.type}`, filename, node: $value, src });
267
267
  } else if ($value.value === '') {
268
268
  logger.error({ message: 'Expected dimension, received empty string', filename, node: $value, src });
269
- } else if (String(Number($value.value)) === $value.value) {
269
+ } else if (String(Number.parseFloat($value.value)) === $value.value) {
270
270
  logger.error({ message: 'Missing units', filename, node: $value, src });
271
- } else if (!/^[0-9]+/.test($value.value)) {
271
+ } else if (!/^-?[0-9]+(\.[0-9]+)?/.test($value.value)) {
272
272
  logger.error({ message: `Expected dimension with units, received ${print($value)}`, filename, node: $value, src });
273
273
  }
274
274
  }
package/parse/yaml.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { DocumentNode } from '@humanwhocodes/momoa';
2
- import type Logger from '../logger.js';
3
-
4
- export interface ParseYAMLOptions {
5
- logger: Logger;
6
- }
7
-
8
- /**
9
- * Take a YAML document and create a Momoa JSON AST from it
10
- */
11
- export default function yamlToAST(input: string, options: ParseYAMLOptions): DocumentNode;
package/parse/yaml.js DELETED
@@ -1,45 +0,0 @@
1
- import { Composer, Parser } from 'yaml';
2
-
3
- /**
4
- * @typedef {import("yaml").YAMLError} YAMLError
5
- */
6
-
7
- /**
8
- * Convert YAML position to line, column
9
- * @param {string} input
10
- * @param {YAMLError{"pos"]} pos
11
- * @return {import("@babel/code-frame").SourceLocation["start"]}
12
- */
13
- function posToLoc(input, pos) {
14
- let line = 1;
15
- let column = 0;
16
- for (let i = 0; i <= (pos[0] || 0); i++) {
17
- const c = input[i];
18
- if (c === '\n') {
19
- line++;
20
- column = 0;
21
- }
22
- column++;
23
- }
24
- return { line, column };
25
- }
26
-
27
- /**
28
- * Take a YAML document and create a Momoa JSON AST from it
29
- */
30
- export default function yamlToAST(input, { logger }) {
31
- const parser = new Parser();
32
- const composer = new Composer();
33
- for (const node of composer.compose(parser.parse(input))) {
34
- if (node.errors) {
35
- for (const error of node.errors) {
36
- logger.error({
37
- label: 'parse:yaml',
38
- message: `${error.code} ${error.message}`,
39
- node: { loc: { start: posToLoc(input, error.pos) } },
40
- source: input,
41
- });
42
- }
43
- }
44
- }
45
- }