stylelint-plugin-rhythmguard 1.7.0 → 1.8.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 +9 -0
- package/README.md +24 -1
- package/package.json +1 -1
- package/src/cli/audit.js +488 -27
- package/src/utils/token-sources.js +418 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.8.0] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `.rhythmguardrc.json` audit config loading with `--config` and `--no-config`.
|
|
14
|
+
- Added external audit token sources with `--token-source`, `--token-source-format`, and `--token-kind`.
|
|
15
|
+
- Added token source parsing for CSS custom properties, Tailwind v4 `@theme`, flat JSON, Style Dictionary JSON, and DTCG JSON.
|
|
16
|
+
- Expanded audit token-contract reporting with loaded source metadata, raw values that match known tokens, and conflicting token values.
|
|
17
|
+
|
|
9
18
|
## [1.7.0] - 2026-05-23
|
|
10
19
|
|
|
11
20
|
### Added
|
package/README.md
CHANGED
|
@@ -93,9 +93,11 @@ npx rhythmguard audit . --ignore "apps/legacy/**" --ignore "vendor/**"
|
|
|
93
93
|
npx rhythmguard audit ./src --write-baseline
|
|
94
94
|
npx rhythmguard audit ./src --since-baseline --fail-on-new-drift
|
|
95
95
|
npx rhythmguard audit ./src --staged --max-findings 0
|
|
96
|
+
npx rhythmguard audit ./src --token-source ./tokens.json
|
|
97
|
+
npx rhythmguard audit ./src --token-source ./theme.css --token-source-format css
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens,
|
|
100
|
+
The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens, repeated raw values that deserve token review, raw values that match known tokens, and conflicting token values. Scan paths are scoped to the directory argument. Use `--ignore`, `.rhythmguardignore`, or `--ignore-path` for generated or legacy subtrees, then add baselines and CI thresholds when you are ready to gate new drift. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
|
|
99
101
|
|
|
100
102
|
```md
|
|
101
103
|
# Rhythmguard Design-System Audit
|
|
@@ -110,6 +112,27 @@ The report covers authored CSS declarations, Tailwind arbitrary spacing values i
|
|
|
110
112
|
| New findings | 3 |
|
|
111
113
|
```
|
|
112
114
|
|
|
115
|
+
### Audit config and external token sources
|
|
116
|
+
|
|
117
|
+
For large codebases, put shared audit settings in `.rhythmguardrc.json`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"audit": {
|
|
122
|
+
"ignore": ["legacy/**", "generated/**"],
|
|
123
|
+
"tokenSources": [
|
|
124
|
+
"./tokens.json",
|
|
125
|
+
{ "path": "./src/theme.css", "format": "css" }
|
|
126
|
+
],
|
|
127
|
+
"tokenKind": "spacing",
|
|
128
|
+
"tokenCandidateMinCount": 2,
|
|
129
|
+
"minCleanliness": 90
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`rhythmguard audit` loads `.rhythmguardrc.json` automatically when present. Use `--config <file>` for another config, `--no-config` to skip config discovery, and `--token-source <file>` for extra canonical token files. Token source paths in config files resolve from the config file directory; CLI token source paths resolve from the current working directory. Supported source formats are CSS custom properties and Tailwind v4 `@theme`, flat JSON maps, Style Dictionary JSON, and DTCG JSON.
|
|
135
|
+
|
|
113
136
|
## Installation
|
|
114
137
|
|
|
115
138
|
```bash
|
package/package.json
CHANGED
package/src/cli/audit.js
CHANGED
|
@@ -6,6 +6,14 @@ const path = require('node:path');
|
|
|
6
6
|
|
|
7
7
|
const { formatLength } = require('../utils/length');
|
|
8
8
|
const { createTailwindClassAnalyzer } = require('../utils/tailwind-class-analysis');
|
|
9
|
+
const {
|
|
10
|
+
addDefinition,
|
|
11
|
+
createTokenKindMatcher,
|
|
12
|
+
getNormalizedValueKeys,
|
|
13
|
+
normalizeTokenKind,
|
|
14
|
+
normalizeTokenSourceFormat,
|
|
15
|
+
parseTokenSources,
|
|
16
|
+
} = require('../utils/token-sources');
|
|
9
17
|
|
|
10
18
|
const args = process.argv.slice(3);
|
|
11
19
|
const pluginPath = path.resolve(__dirname, '..', 'index.js');
|
|
@@ -13,8 +21,8 @@ const pluginPath = path.resolve(__dirname, '..', 'index.js');
|
|
|
13
21
|
const DEFAULT_SCALE = [0, 4, 8, 12, 16, 24, 32];
|
|
14
22
|
const DEFAULT_BASE_FONT_SIZE = 16;
|
|
15
23
|
const DEFAULT_BASELINE_PATH = '.rhythmguard-baseline.json';
|
|
24
|
+
const DEFAULT_CONFIG_PATH = '.rhythmguardrc.json';
|
|
16
25
|
const DEFAULT_IGNORE_PATH = '.rhythmguardignore';
|
|
17
|
-
const DEFAULT_TOKEN_PATTERN = '^--spac(e|ing)-';
|
|
18
26
|
const DEFAULT_TOKEN_CANDIDATE_MIN_COUNT = 2;
|
|
19
27
|
const VALID_FORMATS = new Set(['text', 'json', 'markdown']);
|
|
20
28
|
const SKIP_DIRS = new Set([
|
|
@@ -49,6 +57,8 @@ Options:
|
|
|
49
57
|
--format <text|json|markdown> Output format (default: text)
|
|
50
58
|
--json Alias for --format json
|
|
51
59
|
--markdown Alias for --format markdown
|
|
60
|
+
--config <file> Load audit config (default: .rhythmguardrc.json when present)
|
|
61
|
+
--no-config Ignore .rhythmguardrc.json discovery
|
|
52
62
|
--ignore <pattern> Exclude root-relative path/glob (repeatable, comma-separated)
|
|
53
63
|
--ignore-path <file> Load ignore patterns from file (default: .rhythmguardignore when present)
|
|
54
64
|
--baseline <file> Baseline file path (default: .rhythmguard-baseline.json)
|
|
@@ -59,6 +69,9 @@ Options:
|
|
|
59
69
|
--min-cleanliness <percent> Exit 1 when scale cleanliness is lower than this percent
|
|
60
70
|
--since <git-ref> Scan only changed files since a git ref
|
|
61
71
|
--staged Scan only staged files
|
|
72
|
+
--token-source <file> External token source (repeatable, comma-separated)
|
|
73
|
+
--token-source-format <format> Token source format: auto, css, flat-json, style-dictionary, dtcg (default: auto)
|
|
74
|
+
--token-kind <kind> Token kind: spacing, radius, typography, size, all (default: spacing)
|
|
62
75
|
--token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
|
|
63
76
|
--scale <values> Comma-separated scale values (default: 0,4,8,12,16,24,32)
|
|
64
77
|
--base-font-size <number> px base for rem/em conversion (default: 16)
|
|
@@ -68,6 +81,9 @@ function parseArgs(argv) {
|
|
|
68
81
|
const parsed = {
|
|
69
82
|
baselinePath: DEFAULT_BASELINE_PATH,
|
|
70
83
|
baseFontSize: DEFAULT_BASE_FONT_SIZE,
|
|
84
|
+
cliOptions: new Set(),
|
|
85
|
+
configExplicit: false,
|
|
86
|
+
configPath: DEFAULT_CONFIG_PATH,
|
|
71
87
|
dir: null,
|
|
72
88
|
failOnNewDrift: false,
|
|
73
89
|
format: 'text',
|
|
@@ -75,11 +91,15 @@ function parseArgs(argv) {
|
|
|
75
91
|
ignorePatterns: [],
|
|
76
92
|
maxFindings: null,
|
|
77
93
|
minCleanliness: null,
|
|
94
|
+
noConfig: false,
|
|
78
95
|
scale: DEFAULT_SCALE,
|
|
79
96
|
since: null,
|
|
80
97
|
sinceBaseline: false,
|
|
81
98
|
staged: false,
|
|
82
99
|
tokenCandidateMinCount: DEFAULT_TOKEN_CANDIDATE_MIN_COUNT,
|
|
100
|
+
tokenKind: 'spacing',
|
|
101
|
+
tokenSourceFormat: 'auto',
|
|
102
|
+
tokenSources: [],
|
|
83
103
|
writeBaseline: false,
|
|
84
104
|
};
|
|
85
105
|
|
|
@@ -103,38 +123,63 @@ function parseArgs(argv) {
|
|
|
103
123
|
|
|
104
124
|
if (arg === '--ignore') {
|
|
105
125
|
parsed.ignorePatterns.push(...parseIgnorePatterns(argv[++index]));
|
|
126
|
+
parsed.cliOptions.add('ignore');
|
|
106
127
|
continue;
|
|
107
128
|
}
|
|
108
129
|
|
|
109
130
|
if (arg.startsWith('--ignore=')) {
|
|
110
131
|
parsed.ignorePatterns.push(...parseIgnorePatterns(arg.slice('--ignore='.length)));
|
|
132
|
+
parsed.cliOptions.add('ignore');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (arg === '--config') {
|
|
137
|
+
parsed.configPath = parsePathOption(argv[++index], '--config');
|
|
138
|
+
parsed.configExplicit = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg.startsWith('--config=')) {
|
|
143
|
+
parsed.configPath = parsePathOption(arg.slice('--config='.length), '--config');
|
|
144
|
+
parsed.configExplicit = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (arg === '--no-config') {
|
|
149
|
+
parsed.noConfig = true;
|
|
111
150
|
continue;
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
if (arg === '--ignore-path') {
|
|
115
154
|
parsed.ignorePath = parsePathOption(argv[++index], '--ignore-path');
|
|
155
|
+
parsed.cliOptions.add('ignorePath');
|
|
116
156
|
continue;
|
|
117
157
|
}
|
|
118
158
|
|
|
119
159
|
if (arg.startsWith('--ignore-path=')) {
|
|
120
160
|
parsed.ignorePath = parsePathOption(arg.slice('--ignore-path='.length), '--ignore-path');
|
|
161
|
+
parsed.cliOptions.add('ignorePath');
|
|
121
162
|
continue;
|
|
122
163
|
}
|
|
123
164
|
|
|
124
165
|
if (arg === '--baseline') {
|
|
125
166
|
parsed.baselinePath = parsePathOption(argv[++index], '--baseline');
|
|
167
|
+
parsed.cliOptions.add('baselinePath');
|
|
126
168
|
continue;
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
if (arg.startsWith('--baseline=')) {
|
|
130
172
|
parsed.baselinePath = parsePathOption(arg.slice('--baseline='.length), '--baseline');
|
|
173
|
+
parsed.cliOptions.add('baselinePath');
|
|
131
174
|
continue;
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
if (arg === '--write-baseline') {
|
|
135
178
|
parsed.writeBaseline = true;
|
|
179
|
+
parsed.cliOptions.add('writeBaseline');
|
|
136
180
|
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
137
181
|
parsed.baselinePath = parsePathOption(argv[++index], '--write-baseline');
|
|
182
|
+
parsed.cliOptions.add('baselinePath');
|
|
138
183
|
}
|
|
139
184
|
continue;
|
|
140
185
|
}
|
|
@@ -142,13 +187,17 @@ function parseArgs(argv) {
|
|
|
142
187
|
if (arg.startsWith('--write-baseline=')) {
|
|
143
188
|
parsed.writeBaseline = true;
|
|
144
189
|
parsed.baselinePath = parsePathOption(arg.slice('--write-baseline='.length), '--write-baseline');
|
|
190
|
+
parsed.cliOptions.add('writeBaseline');
|
|
191
|
+
parsed.cliOptions.add('baselinePath');
|
|
145
192
|
continue;
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
if (arg === '--since-baseline') {
|
|
149
196
|
parsed.sinceBaseline = true;
|
|
197
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
150
198
|
if (argv[index + 1] && !argv[index + 1].startsWith('-')) {
|
|
151
199
|
parsed.baselinePath = parsePathOption(argv[++index], '--since-baseline');
|
|
200
|
+
parsed.cliOptions.add('baselinePath');
|
|
152
201
|
}
|
|
153
202
|
continue;
|
|
154
203
|
}
|
|
@@ -156,26 +205,32 @@ function parseArgs(argv) {
|
|
|
156
205
|
if (arg.startsWith('--since-baseline=')) {
|
|
157
206
|
parsed.sinceBaseline = true;
|
|
158
207
|
parsed.baselinePath = parsePathOption(arg.slice('--since-baseline='.length), '--since-baseline');
|
|
208
|
+
parsed.cliOptions.add('sinceBaseline');
|
|
209
|
+
parsed.cliOptions.add('baselinePath');
|
|
159
210
|
continue;
|
|
160
211
|
}
|
|
161
212
|
|
|
162
213
|
if (arg === '--fail-on-new-drift') {
|
|
163
214
|
parsed.failOnNewDrift = true;
|
|
215
|
+
parsed.cliOptions.add('failOnNewDrift');
|
|
164
216
|
continue;
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
if (arg === '--max-findings') {
|
|
168
220
|
parsed.maxFindings = parseNonNegativeInteger(argv[++index], '--max-findings');
|
|
221
|
+
parsed.cliOptions.add('maxFindings');
|
|
169
222
|
continue;
|
|
170
223
|
}
|
|
171
224
|
|
|
172
225
|
if (arg.startsWith('--max-findings=')) {
|
|
173
226
|
parsed.maxFindings = parseNonNegativeInteger(arg.slice('--max-findings='.length), '--max-findings');
|
|
227
|
+
parsed.cliOptions.add('maxFindings');
|
|
174
228
|
continue;
|
|
175
229
|
}
|
|
176
230
|
|
|
177
231
|
if (arg === '--min-cleanliness') {
|
|
178
232
|
parsed.minCleanliness = parsePercentage(argv[++index], '--min-cleanliness');
|
|
233
|
+
parsed.cliOptions.add('minCleanliness');
|
|
179
234
|
continue;
|
|
180
235
|
}
|
|
181
236
|
|
|
@@ -184,26 +239,67 @@ function parseArgs(argv) {
|
|
|
184
239
|
arg.slice('--min-cleanliness='.length),
|
|
185
240
|
'--min-cleanliness',
|
|
186
241
|
);
|
|
242
|
+
parsed.cliOptions.add('minCleanliness');
|
|
187
243
|
continue;
|
|
188
244
|
}
|
|
189
245
|
|
|
190
246
|
if (arg === '--since') {
|
|
191
247
|
parsed.since = parsePathOption(argv[++index], '--since');
|
|
248
|
+
parsed.cliOptions.add('since');
|
|
192
249
|
continue;
|
|
193
250
|
}
|
|
194
251
|
|
|
195
252
|
if (arg.startsWith('--since=')) {
|
|
196
253
|
parsed.since = parsePathOption(arg.slice('--since='.length), '--since');
|
|
254
|
+
parsed.cliOptions.add('since');
|
|
197
255
|
continue;
|
|
198
256
|
}
|
|
199
257
|
|
|
200
258
|
if (arg === '--staged') {
|
|
201
259
|
parsed.staged = true;
|
|
260
|
+
parsed.cliOptions.add('staged');
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (arg === '--token-source') {
|
|
265
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(argv[++index]));
|
|
266
|
+
parsed.cliOptions.add('tokenSources');
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (arg.startsWith('--token-source=')) {
|
|
271
|
+
parsed.tokenSources.push(...parseTokenSourcePaths(arg.slice('--token-source='.length)));
|
|
272
|
+
parsed.cliOptions.add('tokenSources');
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (arg === '--token-source-format') {
|
|
277
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(argv[++index]);
|
|
278
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (arg.startsWith('--token-source-format=')) {
|
|
283
|
+
parsed.tokenSourceFormat = normalizeTokenSourceFormat(arg.slice('--token-source-format='.length));
|
|
284
|
+
parsed.cliOptions.add('tokenSourceFormat');
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (arg === '--token-kind') {
|
|
289
|
+
parsed.tokenKind = normalizeTokenKind(argv[++index]);
|
|
290
|
+
parsed.cliOptions.add('tokenKind');
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (arg.startsWith('--token-kind=')) {
|
|
295
|
+
parsed.tokenKind = normalizeTokenKind(arg.slice('--token-kind='.length));
|
|
296
|
+
parsed.cliOptions.add('tokenKind');
|
|
202
297
|
continue;
|
|
203
298
|
}
|
|
204
299
|
|
|
205
300
|
if (arg === '--token-candidate-min-count') {
|
|
206
301
|
parsed.tokenCandidateMinCount = parsePositiveInteger(argv[++index], '--token-candidate-min-count');
|
|
302
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
207
303
|
continue;
|
|
208
304
|
}
|
|
209
305
|
|
|
@@ -212,6 +308,7 @@ function parseArgs(argv) {
|
|
|
212
308
|
arg.slice('--token-candidate-min-count='.length),
|
|
213
309
|
'--token-candidate-min-count',
|
|
214
310
|
);
|
|
311
|
+
parsed.cliOptions.add('tokenCandidateMinCount');
|
|
215
312
|
continue;
|
|
216
313
|
}
|
|
217
314
|
|
|
@@ -227,21 +324,25 @@ function parseArgs(argv) {
|
|
|
227
324
|
|
|
228
325
|
if (arg === '--scale') {
|
|
229
326
|
parsed.scale = parseScale(argv[++index]);
|
|
327
|
+
parsed.cliOptions.add('scale');
|
|
230
328
|
continue;
|
|
231
329
|
}
|
|
232
330
|
|
|
233
331
|
if (arg.startsWith('--scale=')) {
|
|
234
332
|
parsed.scale = parseScale(arg.slice('--scale='.length));
|
|
333
|
+
parsed.cliOptions.add('scale');
|
|
235
334
|
continue;
|
|
236
335
|
}
|
|
237
336
|
|
|
238
337
|
if (arg === '--base-font-size') {
|
|
239
338
|
parsed.baseFontSize = parseBaseFontSize(argv[++index]);
|
|
339
|
+
parsed.cliOptions.add('baseFontSize');
|
|
240
340
|
continue;
|
|
241
341
|
}
|
|
242
342
|
|
|
243
343
|
if (arg.startsWith('--base-font-size=')) {
|
|
244
344
|
parsed.baseFontSize = parseBaseFontSize(arg.slice('--base-font-size='.length));
|
|
345
|
+
parsed.cliOptions.add('baseFontSize');
|
|
245
346
|
continue;
|
|
246
347
|
}
|
|
247
348
|
|
|
@@ -319,6 +420,22 @@ function parseIgnorePatterns(raw) {
|
|
|
319
420
|
return patterns;
|
|
320
421
|
}
|
|
321
422
|
|
|
423
|
+
function parseTokenSourcePaths(raw) {
|
|
424
|
+
if (!raw) {
|
|
425
|
+
throw new Error('Missing value for --token-source.');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const sources = String(raw).split(',')
|
|
429
|
+
.map((sourcePath) => sourcePath.trim())
|
|
430
|
+
.filter(Boolean);
|
|
431
|
+
|
|
432
|
+
if (sources.length === 0) {
|
|
433
|
+
throw new Error('--token-source must include at least one file.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return sources;
|
|
437
|
+
}
|
|
438
|
+
|
|
322
439
|
function normalizeIgnorePattern(pattern) {
|
|
323
440
|
return String(pattern)
|
|
324
441
|
.trim()
|
|
@@ -377,6 +494,145 @@ function assertDirectory(dir) {
|
|
|
377
494
|
return resolvedDir;
|
|
378
495
|
}
|
|
379
496
|
|
|
497
|
+
function loadAuditConfig(parsed) {
|
|
498
|
+
if (parsed.noConfig) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const resolvedPath = path.resolve(process.cwd(), parsed.configPath);
|
|
503
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
504
|
+
if (parsed.configExplicit) {
|
|
505
|
+
throw new Error(`Config file not found: ${parsed.configPath}`);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let config;
|
|
511
|
+
try {
|
|
512
|
+
config = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
513
|
+
} catch (err) {
|
|
514
|
+
throw new Error(`Invalid Rhythmguard config ${parsed.configPath}: ${err.message}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
518
|
+
throw new Error(`Invalid Rhythmguard config ${parsed.configPath}: expected an object.`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const audit = config.audit || {};
|
|
522
|
+
if (!audit || typeof audit !== 'object' || Array.isArray(audit)) {
|
|
523
|
+
throw new Error(`Invalid Rhythmguard config ${parsed.configPath}: "audit" must be an object.`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
audit,
|
|
528
|
+
file: formatPath(resolvedPath),
|
|
529
|
+
rootDir: path.dirname(resolvedPath),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function applyAuditConfig(parsed, configResult) {
|
|
534
|
+
const cliOptions = parsed.cliOptions;
|
|
535
|
+
const next = {
|
|
536
|
+
...parsed,
|
|
537
|
+
config: configResult
|
|
538
|
+
? {
|
|
539
|
+
file: configResult.file,
|
|
540
|
+
}
|
|
541
|
+
: null,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (!configResult) {
|
|
545
|
+
next.tokenSources = normalizeCliTokenSources(parsed.tokenSources, parsed.tokenSourceFormat);
|
|
546
|
+
delete next.cliOptions;
|
|
547
|
+
return next;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const audit = configResult.audit;
|
|
551
|
+
|
|
552
|
+
if (Array.isArray(audit.ignore)) {
|
|
553
|
+
next.ignorePatterns = [
|
|
554
|
+
...audit.ignore.map((pattern) => normalizeIgnorePattern(pattern)).filter(Boolean),
|
|
555
|
+
...parsed.ignorePatterns,
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const configuredTokenSources = normalizeConfigTokenSources(
|
|
560
|
+
audit.tokenSources,
|
|
561
|
+
configResult.rootDir,
|
|
562
|
+
);
|
|
563
|
+
next.tokenSources = [
|
|
564
|
+
...configuredTokenSources,
|
|
565
|
+
...normalizeCliTokenSources(parsed.tokenSources, parsed.tokenSourceFormat),
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
applyConfigScalar(next, audit, cliOptions, 'ignorePath', 'ignorePath', parsePathOption);
|
|
569
|
+
applyConfigScalar(next, audit, cliOptions, 'baselinePath', 'baseline', parsePathOption);
|
|
570
|
+
applyConfigScalar(next, audit, cliOptions, 'maxFindings', 'maxFindings', parseNonNegativeInteger);
|
|
571
|
+
applyConfigScalar(next, audit, cliOptions, 'minCleanliness', 'minCleanliness', parsePercentage);
|
|
572
|
+
applyConfigScalar(next, audit, cliOptions, 'tokenCandidateMinCount', 'tokenCandidateMinCount', parsePositiveInteger);
|
|
573
|
+
applyConfigScalar(next, audit, cliOptions, 'tokenKind', 'tokenKind', normalizeTokenKind);
|
|
574
|
+
applyConfigScalar(next, audit, cliOptions, 'baseFontSize', 'baseFontSize', parseBaseFontSize);
|
|
575
|
+
applyConfigScalar(next, audit, cliOptions, 'scale', 'scale', (value) => {
|
|
576
|
+
if (Array.isArray(value)) {
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
return parseScale(String(value));
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
delete next.cliOptions;
|
|
583
|
+
return next;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function applyConfigScalar(target, audit, cliOptions, targetKey, configKey, parser) {
|
|
587
|
+
if (cliOptions.has(targetKey) || audit[configKey] === undefined) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
target[targetKey] = parser(audit[configKey], configKey);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function normalizeCliTokenSources(sources, format) {
|
|
595
|
+
return sources.map((sourcePath) => ({
|
|
596
|
+
baseDir: process.cwd(),
|
|
597
|
+
format,
|
|
598
|
+
path: sourcePath,
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizeConfigTokenSources(sources, baseDir) {
|
|
603
|
+
if (sources === undefined) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!Array.isArray(sources)) {
|
|
608
|
+
throw new Error('Invalid Rhythmguard config: audit.tokenSources must be an array.');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return sources.map((source) => {
|
|
612
|
+
if (typeof source === 'string') {
|
|
613
|
+
return {
|
|
614
|
+
baseDir,
|
|
615
|
+
format: 'auto',
|
|
616
|
+
path: source,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
621
|
+
throw new Error('Invalid Rhythmguard config: audit.tokenSources entries must be strings or objects.');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (typeof source.path !== 'string' || source.path.trim().length === 0) {
|
|
625
|
+
throw new Error('Invalid Rhythmguard config: token source objects must include a path.');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
baseDir,
|
|
630
|
+
format: normalizeTokenSourceFormat(source.format || 'auto'),
|
|
631
|
+
path: source.path,
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
380
636
|
function loadIgnorePatterns(ignorePath) {
|
|
381
637
|
if (!ignorePath) {
|
|
382
638
|
return [];
|
|
@@ -761,11 +1017,21 @@ function offsetToLineColumn(lineStarts, offset) {
|
|
|
761
1017
|
};
|
|
762
1018
|
}
|
|
763
1019
|
|
|
764
|
-
function collectTokenContract(
|
|
1020
|
+
function collectTokenContract({
|
|
1021
|
+
baseFontSize,
|
|
1022
|
+
cssFiles,
|
|
1023
|
+
cssFindings,
|
|
1024
|
+
externalTokenDefinitions,
|
|
1025
|
+
minCandidateCount,
|
|
1026
|
+
tailwindFindings,
|
|
1027
|
+
tokenKind,
|
|
1028
|
+
tokenSourceReports,
|
|
1029
|
+
}) {
|
|
765
1030
|
const definitions = new Map();
|
|
766
1031
|
const uses = new Map();
|
|
767
1032
|
const rawValues = new Map();
|
|
768
1033
|
const rawValueLocations = new Set();
|
|
1034
|
+
const matchesKind = createTokenKindMatcher(tokenKind);
|
|
769
1035
|
|
|
770
1036
|
for (const filePath of cssFiles) {
|
|
771
1037
|
let source = '';
|
|
@@ -776,8 +1042,20 @@ function collectTokenContract(cssFiles, cssFindings, tailwindFindings, minCandid
|
|
|
776
1042
|
}
|
|
777
1043
|
|
|
778
1044
|
const file = formatPath(filePath);
|
|
779
|
-
collectTokenDefinitions(source, file, definitions);
|
|
780
|
-
collectTokenUses(source, file, uses);
|
|
1045
|
+
collectTokenDefinitions(source, file, definitions, matchesKind, baseFontSize);
|
|
1046
|
+
collectTokenUses(source, file, uses, matchesKind);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
for (const entry of externalTokenDefinitions.values()) {
|
|
1050
|
+
for (const value of entry.values || []) {
|
|
1051
|
+
addDefinition(definitions, {
|
|
1052
|
+
baseFontSize,
|
|
1053
|
+
file: Array.from(entry.files)[0] || 'external-token-source',
|
|
1054
|
+
source: Array.from(entry.sources)[0] || Array.from(entry.files)[0] || 'external-token-source',
|
|
1055
|
+
token: entry.token,
|
|
1056
|
+
value,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
781
1059
|
}
|
|
782
1060
|
|
|
783
1061
|
for (const finding of cssFindings) {
|
|
@@ -792,6 +1070,8 @@ function collectTokenContract(cssFiles, cssFindings, tailwindFindings, minCandid
|
|
|
792
1070
|
const usedTokens = mapTokenEntries(uses);
|
|
793
1071
|
const missingTokens = usedTokens.filter(({ token }) => !definitions.has(token));
|
|
794
1072
|
const unusedTokens = definedTokens.filter(({ token }) => !uses.has(token));
|
|
1073
|
+
const rawValueMatches = collectRawValueMatches(rawValues, definitions, baseFontSize);
|
|
1074
|
+
const conflictingTokens = collectConflictingTokens(definitions);
|
|
795
1075
|
const rawValueCandidates = Array.from(rawValues.entries())
|
|
796
1076
|
.map(([value, entry]) => ({
|
|
797
1077
|
count: entry.count,
|
|
@@ -803,12 +1083,18 @@ function collectTokenContract(cssFiles, cssFindings, tailwindFindings, minCandid
|
|
|
803
1083
|
|
|
804
1084
|
return {
|
|
805
1085
|
definedTokens,
|
|
1086
|
+
conflictingTokens,
|
|
806
1087
|
missingTokens,
|
|
1088
|
+
rawValueMatches,
|
|
807
1089
|
rawValueCandidates,
|
|
1090
|
+
sources: tokenSourceReports,
|
|
808
1091
|
summary: {
|
|
1092
|
+
conflictingTokens: conflictingTokens.length,
|
|
809
1093
|
definedTokens: definedTokens.length,
|
|
810
1094
|
missingTokens: missingTokens.length,
|
|
1095
|
+
rawValueMatches: rawValueMatches.length,
|
|
811
1096
|
rawValueCandidates: rawValueCandidates.length,
|
|
1097
|
+
tokenSources: tokenSourceReports.length,
|
|
812
1098
|
unusedTokens: unusedTokens.length,
|
|
813
1099
|
usedTokens: usedTokens.length,
|
|
814
1100
|
},
|
|
@@ -817,34 +1103,33 @@ function collectTokenContract(cssFiles, cssFindings, tailwindFindings, minCandid
|
|
|
817
1103
|
};
|
|
818
1104
|
}
|
|
819
1105
|
|
|
820
|
-
function collectTokenDefinitions(source, file, definitions) {
|
|
1106
|
+
function collectTokenDefinitions(source, file, definitions, matchesKind, baseFontSize) {
|
|
821
1107
|
const declarationPattern = /(--[\w-]+)\s*:\s*([^;{}]+)/g;
|
|
822
1108
|
let match;
|
|
823
1109
|
|
|
824
1110
|
while ((match = declarationPattern.exec(source)) !== null) {
|
|
825
1111
|
const token = match[1];
|
|
826
|
-
if (!
|
|
1112
|
+
if (!matchesKind(token)) {
|
|
827
1113
|
continue;
|
|
828
1114
|
}
|
|
829
1115
|
|
|
830
|
-
|
|
831
|
-
|
|
1116
|
+
addDefinition(definitions, {
|
|
1117
|
+
baseFontSize,
|
|
1118
|
+
file,
|
|
1119
|
+
source: file,
|
|
832
1120
|
token,
|
|
833
|
-
|
|
834
|
-
};
|
|
835
|
-
entry.files.add(file);
|
|
836
|
-
entry.values.add(match[2].trim());
|
|
837
|
-
definitions.set(token, entry);
|
|
1121
|
+
value: match[2].trim(),
|
|
1122
|
+
});
|
|
838
1123
|
}
|
|
839
1124
|
}
|
|
840
1125
|
|
|
841
|
-
function collectTokenUses(source, file, uses) {
|
|
1126
|
+
function collectTokenUses(source, file, uses, matchesKind) {
|
|
842
1127
|
const varPattern = /var\(\s*(--[\w-]+)/g;
|
|
843
1128
|
let match;
|
|
844
1129
|
|
|
845
1130
|
while ((match = varPattern.exec(source)) !== null) {
|
|
846
1131
|
const token = match[1];
|
|
847
|
-
if (!
|
|
1132
|
+
if (!matchesKind(token)) {
|
|
848
1133
|
continue;
|
|
849
1134
|
}
|
|
850
1135
|
|
|
@@ -857,10 +1142,6 @@ function collectTokenUses(source, file, uses) {
|
|
|
857
1142
|
}
|
|
858
1143
|
}
|
|
859
1144
|
|
|
860
|
-
function isSpacingToken(token) {
|
|
861
|
-
return new RegExp(DEFAULT_TOKEN_PATTERN).test(token);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
1145
|
function addRawValue(rawValues, rawValueLocations, value, finding) {
|
|
865
1146
|
if (!value) {
|
|
866
1147
|
return;
|
|
@@ -892,20 +1173,90 @@ function mapTokenEntries(entries) {
|
|
|
892
1173
|
return Array.from(entries.values())
|
|
893
1174
|
.map((entry) => ({
|
|
894
1175
|
files: Array.from(entry.files).sort(),
|
|
1176
|
+
normalizedValues: entry.normalizedValues
|
|
1177
|
+
? Array.from(entry.normalizedValues).sort()
|
|
1178
|
+
: undefined,
|
|
1179
|
+
sources: entry.sources ? Array.from(entry.sources).sort() : undefined,
|
|
895
1180
|
token: entry.token,
|
|
896
1181
|
values: entry.values ? Array.from(entry.values).sort() : undefined,
|
|
897
1182
|
}))
|
|
898
1183
|
.sort((a, b) => a.token.localeCompare(b.token));
|
|
899
1184
|
}
|
|
900
1185
|
|
|
1186
|
+
function collectRawValueMatches(rawValues, definitions, baseFontSize) {
|
|
1187
|
+
const valueToTokens = createValueToTokensMap(definitions);
|
|
1188
|
+
const matches = [];
|
|
1189
|
+
|
|
1190
|
+
for (const [value, entry] of rawValues.entries()) {
|
|
1191
|
+
const tokens = new Set();
|
|
1192
|
+
for (const key of getNormalizedValueKeys(value, baseFontSize)) {
|
|
1193
|
+
for (const token of valueToTokens.get(key) || []) {
|
|
1194
|
+
tokens.add(token);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (tokens.size === 0) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
matches.push({
|
|
1203
|
+
count: entry.count,
|
|
1204
|
+
files: Array.from(entry.files).sort(),
|
|
1205
|
+
tokens: Array.from(tokens).sort(),
|
|
1206
|
+
value,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return matches.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function collectConflictingTokens(definitions) {
|
|
1214
|
+
const valueToTokens = createValueToTokensMap(definitions);
|
|
1215
|
+
const conflicts = [];
|
|
1216
|
+
|
|
1217
|
+
for (const [value, tokens] of valueToTokens.entries()) {
|
|
1218
|
+
const uniqueTokens = Array.from(tokens).sort();
|
|
1219
|
+
if (uniqueTokens.length < 2) {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
conflicts.push({
|
|
1224
|
+
tokens: uniqueTokens,
|
|
1225
|
+
value,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return conflicts.sort((a, b) => a.value.localeCompare(b.value));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function createValueToTokensMap(definitions) {
|
|
1233
|
+
const valueToTokens = new Map();
|
|
1234
|
+
|
|
1235
|
+
for (const entry of definitions.values()) {
|
|
1236
|
+
for (const value of entry.normalizedValues || []) {
|
|
1237
|
+
const tokens = valueToTokens.get(value) || new Set();
|
|
1238
|
+
tokens.add(entry.token);
|
|
1239
|
+
valueToTokens.set(value, tokens);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return valueToTokens;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
901
1246
|
function buildReport({
|
|
1247
|
+
baseFontSize,
|
|
1248
|
+
config,
|
|
902
1249
|
cssFiles,
|
|
903
1250
|
cssFindings,
|
|
904
1251
|
dir,
|
|
1252
|
+
externalTokenDefinitions,
|
|
905
1253
|
scanScope,
|
|
906
1254
|
templateFiles,
|
|
907
1255
|
tailwindFindings,
|
|
908
1256
|
tokenCandidateMinCount,
|
|
1257
|
+
tokenKind,
|
|
1258
|
+
tokenSourceReports,
|
|
1259
|
+
tokenSourceWarnings,
|
|
909
1260
|
}) {
|
|
910
1261
|
const offScaleValues = countByValue(cssFindings
|
|
911
1262
|
.filter((finding) => finding.type === 'off-scale' && finding.value)
|
|
@@ -930,14 +1281,19 @@ function buildReport({
|
|
|
930
1281
|
const scaleCleanliness = totalFiles > 0
|
|
931
1282
|
? Math.max(0, Math.round(((totalFiles - filesWithIssues) / totalFiles) * 100))
|
|
932
1283
|
: 100;
|
|
933
|
-
const tokenContract = collectTokenContract(
|
|
1284
|
+
const tokenContract = collectTokenContract({
|
|
1285
|
+
baseFontSize,
|
|
934
1286
|
cssFiles,
|
|
935
1287
|
cssFindings,
|
|
1288
|
+
externalTokenDefinitions,
|
|
1289
|
+
minCandidateCount: tokenCandidateMinCount,
|
|
936
1290
|
tailwindFindings,
|
|
937
|
-
|
|
938
|
-
|
|
1291
|
+
tokenKind,
|
|
1292
|
+
tokenSourceReports,
|
|
1293
|
+
});
|
|
939
1294
|
|
|
940
1295
|
return {
|
|
1296
|
+
config,
|
|
941
1297
|
cssFilesScanned: cssFiles.length,
|
|
942
1298
|
directory: dir,
|
|
943
1299
|
filesWithIssues,
|
|
@@ -945,7 +1301,7 @@ function buildReport({
|
|
|
945
1301
|
css: cssFindings,
|
|
946
1302
|
tailwind: tailwindFindings,
|
|
947
1303
|
},
|
|
948
|
-
formatVersion:
|
|
1304
|
+
formatVersion: 4,
|
|
949
1305
|
offScaleValues: Object.fromEntries(sortCountMap(offScaleValues).slice(0, 10)),
|
|
950
1306
|
scaleCleanliness,
|
|
951
1307
|
scanScope,
|
|
@@ -957,17 +1313,20 @@ function buildReport({
|
|
|
957
1313
|
summary: {
|
|
958
1314
|
cssWarnings: cssFindings.length,
|
|
959
1315
|
filesWithIssues,
|
|
1316
|
+
rawValueMatches: tokenContract.summary.rawValueMatches,
|
|
960
1317
|
missingTokens: tokenContract.summary.missingTokens,
|
|
961
1318
|
rawValueCandidates: tokenContract.summary.rawValueCandidates,
|
|
962
1319
|
scaleCleanliness,
|
|
963
1320
|
tailwindArbitrarySpacing: tailwindFindings.length,
|
|
964
1321
|
tokenOpportunities: sumCounts(tokenOpportunities),
|
|
1322
|
+
tokenSources: tokenContract.summary.tokenSources,
|
|
965
1323
|
totalFindings: totalWarnings,
|
|
966
1324
|
unusedTokens: tokenContract.summary.unusedTokens,
|
|
967
1325
|
},
|
|
968
1326
|
tailwindArbitraryValues: Object.fromEntries(sortCountMap(tailwindArbitraryValues).slice(0, 10)),
|
|
969
1327
|
templateFilesScanned: templateFiles.length,
|
|
970
1328
|
tokenContract,
|
|
1329
|
+
tokenSourceWarnings,
|
|
971
1330
|
tokenOpportunities: Object.fromEntries(sortCountMap(tokenOpportunities).slice(0, 10)),
|
|
972
1331
|
topAffectedFiles: topAffectedFiles.map(([file, count]) => ({ count, file })),
|
|
973
1332
|
totalFiles,
|
|
@@ -1136,14 +1495,36 @@ function renderText(report) {
|
|
|
1136
1495
|
}
|
|
1137
1496
|
|
|
1138
1497
|
function appendTokenContractText(lines, tokenContract) {
|
|
1139
|
-
const {
|
|
1140
|
-
|
|
1498
|
+
const {
|
|
1499
|
+
conflictingTokens,
|
|
1500
|
+
missingTokens,
|
|
1501
|
+
rawValueCandidates,
|
|
1502
|
+
rawValueMatches,
|
|
1503
|
+
sources,
|
|
1504
|
+
unusedTokens,
|
|
1505
|
+
} = tokenContract;
|
|
1506
|
+
if (
|
|
1507
|
+
missingTokens.length === 0 &&
|
|
1508
|
+
rawValueCandidates.length === 0 &&
|
|
1509
|
+
rawValueMatches.length === 0 &&
|
|
1510
|
+
unusedTokens.length === 0 &&
|
|
1511
|
+
conflictingTokens.length === 0 &&
|
|
1512
|
+
sources.length === 0
|
|
1513
|
+
) {
|
|
1141
1514
|
return;
|
|
1142
1515
|
}
|
|
1143
1516
|
|
|
1144
1517
|
lines.push(' ── TOKEN CONTRACT ──');
|
|
1145
1518
|
lines.push('');
|
|
1146
1519
|
|
|
1520
|
+
if (sources.length > 0) {
|
|
1521
|
+
lines.push(` Token sources ${sources.length}`);
|
|
1522
|
+
for (const source of sources.slice(0, 5)) {
|
|
1523
|
+
const warningSuffix = source.warnings.length > 0 ? ' (warning)' : '';
|
|
1524
|
+
lines.push(` ${truncate(source.file, 42)} ${source.tokenCount}${warningSuffix}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1147
1528
|
if (missingTokens.length > 0) {
|
|
1148
1529
|
lines.push(` Missing tokens ${missingTokens.length}`);
|
|
1149
1530
|
for (const entry of missingTokens.slice(0, 5)) {
|
|
@@ -1165,6 +1546,20 @@ function appendTokenContractText(lines, tokenContract) {
|
|
|
1165
1546
|
}
|
|
1166
1547
|
}
|
|
1167
1548
|
|
|
1549
|
+
if (rawValueMatches.length > 0) {
|
|
1550
|
+
lines.push(` Raw values matching tokens ${rawValueMatches.length}`);
|
|
1551
|
+
for (const entry of rawValueMatches.slice(0, 5)) {
|
|
1552
|
+
lines.push(` ${entry.value.padEnd(14)} ${truncate(entry.tokens.join(', '), 34)}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (conflictingTokens.length > 0) {
|
|
1557
|
+
lines.push(` Conflicting tokens ${conflictingTokens.length}`);
|
|
1558
|
+
for (const entry of conflictingTokens.slice(0, 5)) {
|
|
1559
|
+
lines.push(` ${entry.value.padEnd(14)} ${truncate(entry.tokens.join(', '), 34)}`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1168
1563
|
lines.push('');
|
|
1169
1564
|
}
|
|
1170
1565
|
|
|
@@ -1267,14 +1662,39 @@ function renderMarkdown(report) {
|
|
|
1267
1662
|
}
|
|
1268
1663
|
|
|
1269
1664
|
function appendTokenContractMarkdown(lines, tokenContract) {
|
|
1270
|
-
const {
|
|
1271
|
-
|
|
1665
|
+
const {
|
|
1666
|
+
conflictingTokens,
|
|
1667
|
+
missingTokens,
|
|
1668
|
+
rawValueCandidates,
|
|
1669
|
+
rawValueMatches,
|
|
1670
|
+
sources,
|
|
1671
|
+
unusedTokens,
|
|
1672
|
+
} = tokenContract;
|
|
1673
|
+
if (
|
|
1674
|
+
missingTokens.length === 0 &&
|
|
1675
|
+
rawValueCandidates.length === 0 &&
|
|
1676
|
+
rawValueMatches.length === 0 &&
|
|
1677
|
+
unusedTokens.length === 0 &&
|
|
1678
|
+
conflictingTokens.length === 0 &&
|
|
1679
|
+
sources.length === 0
|
|
1680
|
+
) {
|
|
1272
1681
|
return;
|
|
1273
1682
|
}
|
|
1274
1683
|
|
|
1275
1684
|
lines.push('## Token Contract');
|
|
1276
1685
|
lines.push('');
|
|
1277
1686
|
|
|
1687
|
+
if (sources.length > 0) {
|
|
1688
|
+
lines.push('### Token Sources');
|
|
1689
|
+
lines.push('');
|
|
1690
|
+
lines.push('| File | Format | Tokens | Warnings |');
|
|
1691
|
+
lines.push('| --- | --- | ---: | --- |');
|
|
1692
|
+
for (const source of sources.slice(0, 10)) {
|
|
1693
|
+
lines.push(`| \`${escapeMarkdown(source.file)}\` | \`${source.format}\` | ${source.tokenCount} | \`${escapeMarkdown(source.warnings.join('; ') || 'none')}\` |`);
|
|
1694
|
+
}
|
|
1695
|
+
lines.push('');
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1278
1698
|
if (missingTokens.length > 0) {
|
|
1279
1699
|
lines.push('### Tokens Used But Missing');
|
|
1280
1700
|
lines.push('');
|
|
@@ -1307,6 +1727,28 @@ function appendTokenContractMarkdown(lines, tokenContract) {
|
|
|
1307
1727
|
}
|
|
1308
1728
|
lines.push('');
|
|
1309
1729
|
}
|
|
1730
|
+
|
|
1731
|
+
if (rawValueMatches.length > 0) {
|
|
1732
|
+
lines.push('### Raw Values Matching Known Tokens');
|
|
1733
|
+
lines.push('');
|
|
1734
|
+
lines.push('| Value | Count | Tokens |');
|
|
1735
|
+
lines.push('| --- | ---: | --- |');
|
|
1736
|
+
for (const entry of rawValueMatches.slice(0, 10)) {
|
|
1737
|
+
lines.push(`| \`${escapeMarkdown(entry.value)}\` | ${entry.count} | \`${escapeMarkdown(entry.tokens.join(', '))}\` |`);
|
|
1738
|
+
}
|
|
1739
|
+
lines.push('');
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (conflictingTokens.length > 0) {
|
|
1743
|
+
lines.push('### Conflicting Token Values');
|
|
1744
|
+
lines.push('');
|
|
1745
|
+
lines.push('| Value | Tokens |');
|
|
1746
|
+
lines.push('| --- | --- |');
|
|
1747
|
+
for (const entry of conflictingTokens.slice(0, 10)) {
|
|
1748
|
+
lines.push(`| \`${escapeMarkdown(entry.value)}\` | \`${escapeMarkdown(entry.tokens.join(', '))}\` |`);
|
|
1749
|
+
}
|
|
1750
|
+
lines.push('');
|
|
1751
|
+
}
|
|
1310
1752
|
}
|
|
1311
1753
|
|
|
1312
1754
|
function appendBaselineMarkdown(lines, report) {
|
|
@@ -1400,6 +1842,13 @@ async function run() {
|
|
|
1400
1842
|
return;
|
|
1401
1843
|
}
|
|
1402
1844
|
|
|
1845
|
+
try {
|
|
1846
|
+
parsed = applyAuditConfig(parsed, loadAuditConfig(parsed));
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
process.stderr.write(`${err.message}\n`);
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1403
1852
|
const resolvedDir = assertDirectory(parsed.dir);
|
|
1404
1853
|
let ignorePatterns;
|
|
1405
1854
|
try {
|
|
@@ -1434,14 +1883,26 @@ async function run() {
|
|
|
1434
1883
|
process.exit(1);
|
|
1435
1884
|
}
|
|
1436
1885
|
|
|
1886
|
+
const tokenSourceResult = parseTokenSources({
|
|
1887
|
+
baseFontSize: parsed.baseFontSize,
|
|
1888
|
+
sources: parsed.tokenSources,
|
|
1889
|
+
tokenKind: parsed.tokenKind,
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1437
1892
|
const report = buildReport({
|
|
1893
|
+
baseFontSize: parsed.baseFontSize,
|
|
1894
|
+
config: parsed.config,
|
|
1438
1895
|
cssFiles,
|
|
1439
1896
|
cssFindings: collectCssFindings(cssResults),
|
|
1440
1897
|
dir: parsed.dir,
|
|
1898
|
+
externalTokenDefinitions: tokenSourceResult.definitions,
|
|
1441
1899
|
scanScope,
|
|
1442
1900
|
tailwindFindings: collectTailwindFindings(templateFiles, options),
|
|
1443
1901
|
templateFiles,
|
|
1444
1902
|
tokenCandidateMinCount: parsed.tokenCandidateMinCount,
|
|
1903
|
+
tokenKind: parsed.tokenKind,
|
|
1904
|
+
tokenSourceReports: tokenSourceResult.sources,
|
|
1905
|
+
tokenSourceWarnings: tokenSourceResult.warnings,
|
|
1445
1906
|
});
|
|
1446
1907
|
|
|
1447
1908
|
try {
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
formatLength,
|
|
8
|
+
parseLengthToken,
|
|
9
|
+
toPx,
|
|
10
|
+
} = require('./length');
|
|
11
|
+
|
|
12
|
+
const VALID_TOKEN_SOURCE_FORMATS = new Set([
|
|
13
|
+
'auto',
|
|
14
|
+
'css',
|
|
15
|
+
'flat-json',
|
|
16
|
+
'style-dictionary',
|
|
17
|
+
'dtcg',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const VALID_TOKEN_KINDS = new Set([
|
|
21
|
+
'spacing',
|
|
22
|
+
'radius',
|
|
23
|
+
'typography',
|
|
24
|
+
'size',
|
|
25
|
+
'all',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const TOKEN_KIND_PATTERNS = Object.freeze({
|
|
29
|
+
all: /^--/,
|
|
30
|
+
radius: /^--radius-/,
|
|
31
|
+
size: /^--(?:size|width|height|container)-/,
|
|
32
|
+
spacing: /^--(?:space|spacing)-/,
|
|
33
|
+
typography: /^--(?:font|font-size|line-height|leading|tracking|typography)-/,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return (
|
|
38
|
+
value !== null &&
|
|
39
|
+
typeof value === 'object' &&
|
|
40
|
+
!Array.isArray(value)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeTokenSourceFormat(format) {
|
|
45
|
+
const normalized = String(format || 'auto').trim().toLowerCase();
|
|
46
|
+
if (!VALID_TOKEN_SOURCE_FORMATS.has(normalized)) {
|
|
47
|
+
throw new Error(`Invalid token source format "${format}". Expected auto, css, flat-json, style-dictionary, or dtcg.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeTokenKind(kind) {
|
|
54
|
+
const normalized = String(kind || 'spacing').trim().toLowerCase();
|
|
55
|
+
if (!VALID_TOKEN_KINDS.has(normalized)) {
|
|
56
|
+
throw new Error(`Invalid token kind "${kind}". Expected spacing, radius, typography, size, or all.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createTokenKindMatcher(kind) {
|
|
63
|
+
const normalizedKind = normalizeTokenKind(kind);
|
|
64
|
+
const pattern = TOKEN_KIND_PATTERNS[normalizedKind] || TOKEN_KIND_PATTERNS.spacing;
|
|
65
|
+
|
|
66
|
+
return (token) => pattern.test(token);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseTokenSources({
|
|
70
|
+
baseFontSize = 16,
|
|
71
|
+
sources = [],
|
|
72
|
+
tokenKind = 'spacing',
|
|
73
|
+
} = {}) {
|
|
74
|
+
const definitions = new Map();
|
|
75
|
+
const sourceReports = [];
|
|
76
|
+
const warnings = [];
|
|
77
|
+
const matchesKind = createTokenKindMatcher(tokenKind);
|
|
78
|
+
|
|
79
|
+
for (const source of sources) {
|
|
80
|
+
const normalizedSource = normalizeTokenSource(source);
|
|
81
|
+
const sourceReport = {
|
|
82
|
+
file: formatPath(normalizedSource.resolvedPath),
|
|
83
|
+
format: normalizedSource.format,
|
|
84
|
+
requestedFormat: normalizedSource.requestedFormat,
|
|
85
|
+
tokenCount: 0,
|
|
86
|
+
warnings: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(normalizedSource.resolvedPath)) {
|
|
90
|
+
const warning = `Token source not found: ${normalizedSource.displayPath}`;
|
|
91
|
+
sourceReport.warnings.push(warning);
|
|
92
|
+
warnings.push(warning);
|
|
93
|
+
sourceReports.push(sourceReport);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = fs.readFileSync(normalizedSource.resolvedPath, 'utf8');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const warning = `Unable to read token source ${normalizedSource.displayPath}: ${err.message}`;
|
|
102
|
+
sourceReport.warnings.push(warning);
|
|
103
|
+
warnings.push(warning);
|
|
104
|
+
sourceReports.push(sourceReport);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let tokens = [];
|
|
109
|
+
try {
|
|
110
|
+
if (normalizedSource.format === 'css') {
|
|
111
|
+
tokens = collectCssTokens(text, matchesKind);
|
|
112
|
+
} else {
|
|
113
|
+
const parsed = JSON.parse(text);
|
|
114
|
+
const detectedFormat = normalizedSource.requestedFormat === 'auto'
|
|
115
|
+
? detectJsonTokenFormat(parsed)
|
|
116
|
+
: normalizedSource.format;
|
|
117
|
+
sourceReport.format = detectedFormat;
|
|
118
|
+
tokens = collectJsonTokens(parsed, matchesKind);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const warning = `Unable to parse token source ${normalizedSource.displayPath}: ${err.message}`;
|
|
122
|
+
sourceReport.warnings.push(warning);
|
|
123
|
+
warnings.push(warning);
|
|
124
|
+
sourceReports.push(sourceReport);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const token of tokens) {
|
|
129
|
+
addDefinition(definitions, {
|
|
130
|
+
baseFontSize,
|
|
131
|
+
file: sourceReport.file,
|
|
132
|
+
source: sourceReport.file,
|
|
133
|
+
token: token.token,
|
|
134
|
+
value: token.value,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sourceReport.tokenCount = tokens.length;
|
|
139
|
+
sourceReports.push(sourceReport);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
definitions,
|
|
144
|
+
sources: sourceReports,
|
|
145
|
+
warnings,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeTokenSource(source) {
|
|
150
|
+
if (!isPlainObject(source)) {
|
|
151
|
+
throw new Error('Token source entries must be strings or objects with a path.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sourcePath = source.path || source.file;
|
|
155
|
+
if (typeof sourcePath !== 'string' || sourcePath.trim().length === 0) {
|
|
156
|
+
throw new Error('Token source entries must include a non-empty path.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const baseDir = source.baseDir || process.cwd();
|
|
160
|
+
const requestedFormat = normalizeTokenSourceFormat(source.format || 'auto');
|
|
161
|
+
const resolvedPath = path.resolve(baseDir, sourcePath);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
displayPath: sourcePath,
|
|
165
|
+
format: requestedFormat === 'auto' ? detectSourceFormat(resolvedPath) : requestedFormat,
|
|
166
|
+
requestedFormat,
|
|
167
|
+
resolvedPath,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function detectSourceFormat(filePath) {
|
|
172
|
+
if (path.extname(filePath).toLowerCase() === '.css') {
|
|
173
|
+
return 'css';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return 'flat-json';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function detectJsonTokenFormat(parsed) {
|
|
180
|
+
let hasDtcg = false;
|
|
181
|
+
let hasStyleDictionary = false;
|
|
182
|
+
|
|
183
|
+
function walk(value) {
|
|
184
|
+
if (!isPlainObject(value)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Object.prototype.hasOwnProperty.call(value, '$value')) {
|
|
189
|
+
hasDtcg = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (Object.prototype.hasOwnProperty.call(value, 'value')) {
|
|
193
|
+
hasStyleDictionary = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const child of Object.values(value)) {
|
|
197
|
+
walk(child);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
walk(parsed);
|
|
202
|
+
|
|
203
|
+
if (hasDtcg) {
|
|
204
|
+
return 'dtcg';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (hasStyleDictionary) {
|
|
208
|
+
return 'style-dictionary';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return 'flat-json';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collectCssTokens(source, matchesKind) {
|
|
215
|
+
const tokens = [];
|
|
216
|
+
const declarationPattern = /(--[\w-]+)\s*:\s*([^;{}]+)/g;
|
|
217
|
+
let match;
|
|
218
|
+
|
|
219
|
+
while ((match = declarationPattern.exec(source)) !== null) {
|
|
220
|
+
const token = match[1];
|
|
221
|
+
if (!matchesKind(token)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
tokens.push({
|
|
226
|
+
token,
|
|
227
|
+
value: match[2].trim(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return tokens;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function collectJsonTokens(parsed, matchesKind) {
|
|
235
|
+
if (!isPlainObject(parsed)) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tokens = [];
|
|
240
|
+
walkJsonTokenGroup(parsed, [], tokens, matchesKind);
|
|
241
|
+
return tokens;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function walkJsonTokenGroup(group, segments, tokens, matchesKind) {
|
|
245
|
+
for (const [key, value] of Object.entries(group)) {
|
|
246
|
+
if (isMetadataKey(key)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
251
|
+
const tokenEntry = tokenFromPrimitive(key, value, segments);
|
|
252
|
+
if (tokenEntry && matchesKind(tokenEntry.token)) {
|
|
253
|
+
tokens.push(tokenEntry);
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isPlainObject(value)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const leafValue = leafTokenValue(value);
|
|
263
|
+
if (leafValue !== null) {
|
|
264
|
+
const token = key.startsWith('--')
|
|
265
|
+
? key
|
|
266
|
+
: toCustomProperty([...segments, key]);
|
|
267
|
+
if (matchesKind(token)) {
|
|
268
|
+
tokens.push({
|
|
269
|
+
token,
|
|
270
|
+
value: leafValue,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
walkJsonTokenGroup(value, [...segments, key], tokens, matchesKind);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function tokenFromPrimitive(key, value, segments) {
|
|
281
|
+
if (key.startsWith('--')) {
|
|
282
|
+
return {
|
|
283
|
+
token: key,
|
|
284
|
+
value: String(value),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const keyAsLength = parseLengthToken(key);
|
|
289
|
+
const tokenName = typeof value === 'string' ? extractTokenName(value) : null;
|
|
290
|
+
if (keyAsLength && tokenName) {
|
|
291
|
+
return {
|
|
292
|
+
token: tokenName,
|
|
293
|
+
value: key,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const valueAsLength = typeof value === 'string'
|
|
298
|
+
? parseLengthToken(value)
|
|
299
|
+
: typeof value === 'number'
|
|
300
|
+
? parseLengthToken(`${value}px`)
|
|
301
|
+
: null;
|
|
302
|
+
if (valueAsLength) {
|
|
303
|
+
return {
|
|
304
|
+
token: toCustomProperty([...segments, key]),
|
|
305
|
+
value: typeof value === 'number' ? `${value}px` : value,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function leafTokenValue(value) {
|
|
313
|
+
if (typeof value.$value === 'string' || typeof value.$value === 'number') {
|
|
314
|
+
return String(value.$value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (typeof value.value === 'string' || typeof value.value === 'number') {
|
|
318
|
+
return String(value.value);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isMetadataKey(key) {
|
|
325
|
+
return key.startsWith('$')
|
|
326
|
+
|| key === 'type'
|
|
327
|
+
|| key === 'description'
|
|
328
|
+
|| key === 'comment'
|
|
329
|
+
|| key === 'attributes';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function toCustomProperty(segments) {
|
|
333
|
+
return `--${segments
|
|
334
|
+
.map((segment) => String(segment).trim())
|
|
335
|
+
.filter(Boolean)
|
|
336
|
+
.join('-')}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function extractTokenName(value) {
|
|
340
|
+
const varMatch = value.match(/var\(\s*(--[\w-]+)/);
|
|
341
|
+
if (varMatch) {
|
|
342
|
+
return varMatch[1];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (value.startsWith('--')) {
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function addDefinition(definitions, {
|
|
353
|
+
baseFontSize,
|
|
354
|
+
file,
|
|
355
|
+
source,
|
|
356
|
+
token,
|
|
357
|
+
value,
|
|
358
|
+
}) {
|
|
359
|
+
const entry = definitions.get(token) || {
|
|
360
|
+
files: new Set(),
|
|
361
|
+
normalizedValues: new Set(),
|
|
362
|
+
sources: new Set(),
|
|
363
|
+
token,
|
|
364
|
+
values: new Set(),
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
entry.files.add(file);
|
|
368
|
+
entry.sources.add(source);
|
|
369
|
+
entry.values.add(String(value).trim());
|
|
370
|
+
|
|
371
|
+
for (const normalizedValue of getNormalizedValueKeys(value, baseFontSize)) {
|
|
372
|
+
entry.normalizedValues.add(normalizedValue);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
definitions.set(token, entry);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getNormalizedValueKeys(value, baseFontSize = 16) {
|
|
379
|
+
if (value === null || value === undefined) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const raw = String(value).trim();
|
|
384
|
+
if (!raw) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const keys = new Set([raw]);
|
|
389
|
+
const parsed = parseLengthToken(raw);
|
|
390
|
+
if (!parsed) {
|
|
391
|
+
return Array.from(keys);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const absolute = Math.abs(parsed.number);
|
|
395
|
+
keys.add(formatLength(absolute, parsed.unit || 'px'));
|
|
396
|
+
|
|
397
|
+
const px = toPx(absolute, parsed.unit, baseFontSize);
|
|
398
|
+
if (px !== null) {
|
|
399
|
+
keys.add(`${px}px`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return Array.from(keys);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function formatPath(filePath) {
|
|
406
|
+
return path.relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
VALID_TOKEN_KINDS,
|
|
411
|
+
VALID_TOKEN_SOURCE_FORMATS,
|
|
412
|
+
addDefinition,
|
|
413
|
+
createTokenKindMatcher,
|
|
414
|
+
getNormalizedValueKeys,
|
|
415
|
+
normalizeTokenKind,
|
|
416
|
+
normalizeTokenSourceFormat,
|
|
417
|
+
parseTokenSources,
|
|
418
|
+
};
|