stylelint-plugin-rhythmguard 3.2.0 → 3.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +19 -3
  3. package/README.md +2 -1
  4. package/package.json +9 -8
  5. package/src/audit/args.js +103 -307
  6. package/src/audit/config.js +1 -1
  7. package/src/audit/contract.js +6 -28
  8. package/src/audit/index.js +4 -7
  9. package/src/audit/render-markdown.js +3 -0
  10. package/src/audit/render-text.js +2 -1
  11. package/src/audit/report.js +26 -19
  12. package/src/audit/scan/files.js +262 -0
  13. package/src/audit/scan/stylesheets.js +275 -0
  14. package/src/audit/scan/templates.js +175 -0
  15. package/src/cli/doctor.js +1 -1
  16. package/src/cli/quickstart.js +5 -2
  17. package/src/{utils → core}/length.js +24 -1
  18. package/src/{utils → core}/options.js +32 -108
  19. package/src/core/scale-inference.js +560 -0
  20. package/src/{utils → core}/token-packages.json +7 -0
  21. package/src/{utils → core}/token-sources.js +155 -17
  22. package/src/{utils/value-utils.js → core/value-nodes.js} +1 -17
  23. package/src/eslint/rules/tailwind-class-use-motion-scale.js +2 -2
  24. package/src/eslint/rules/tailwind-class-use-scale.js +2 -2
  25. package/src/rules/no-offscale-transform/index.js +22 -91
  26. package/src/rules/prefer-token/index.js +15 -73
  27. package/src/rules/report.js +75 -0
  28. package/src/rules/use-motion-scale/index.js +25 -42
  29. package/src/rules/use-scale/index.js +21 -94
  30. package/src/rules/validate.js +132 -0
  31. package/types/audit.d.ts +11 -0
  32. package/src/audit/scan.js +0 -676
  33. package/src/utils/scale-inference.js +0 -351
  34. /package/src/{utils/constants.js → core/css-vocabulary.js} +0 -0
  35. /package/src/{utils → core}/tailwind-class-analysis.js +0 -0
  36. /package/src/{utils → core}/tailwind-motion-analysis.js +0 -0
  37. /package/src/{utils → core}/time.js +0 -0
  38. /package/src/{utils → core}/token-map.js +0 -0
@@ -38,7 +38,7 @@ const TOKEN_KIND_PATTERNS = Object.freeze({
38
38
  // --spacing) but never letter-/word-spacing. Sass names must start with the scale word
39
39
  // (an optional `system-` prefix allowed): $spacer, $spacers.3, $spacing-01, $system-spacing.
40
40
  // Component variables such as $dropdown-spacer or $card-spacer-y are not scale tokens.
41
- spacing: /^(?:\$(?:system-)?(?:space|spacing|spacer)s?(?:-|$|\.)|--(?:[\w-]*-)?(?<!letter-)(?<!word-)(?:space|spacing)(?:-|$))/,
41
+ spacing: /^(?:\$(?:system-)?(?:space|spacing|spacer)s?(?:-|$|\.)|--(?:[\w-]*-)?(?<!letter-)(?<!word-)(?:space|spacing|spacer)(?:-|$))/,
42
42
  typography: /^--(?:font|font-size|line-height|leading|tracking|typography)-/,
43
43
  });
44
44
 
@@ -193,7 +193,8 @@ function normalizeTokenSource(source) {
193
193
  }
194
194
 
195
195
  function detectSourceFormat(filePath) {
196
- if (path.extname(filePath).toLowerCase() === '.css') {
196
+ // .scss token files are read by the CSS collector too: custom properties plus Sass variables and maps.
197
+ if (['.css', '.scss'].includes(path.extname(filePath).toLowerCase())) {
197
198
  return 'css';
198
199
  }
199
200
 
@@ -237,23 +238,107 @@ function detectJsonTokenFormat(parsed) {
237
238
 
238
239
  function collectCssTokens(source, matchesKind) {
239
240
  const tokens = [];
240
- const declarationPattern = /(--[\w-]+)\s*:\s*([^;{}]+)/g;
241
- let match;
241
+ for (const declaration of customPropertyDeclarations(source)) {
242
+ if (matchesKind(declaration.token)) {
243
+ tokens.push(declaration);
244
+ }
245
+ }
246
+ for (const sassToken of collectScssTokens(source, matchesKind)) {
247
+ tokens.push({ ...sassToken, scope: 'root' });
248
+ }
249
+ return tokens;
250
+ }
242
251
 
243
- while ((match = declarationPattern.exec(source)) !== null) {
244
- const token = match[1];
245
- if (!matchesKind(token)) {
246
- continue;
252
+ const ROOT_SELECTOR = /^(?::root|html|:host|:(?:where|is)\(\s*(?::root|html)\s*\))$/i;
253
+ // At-rules that only condition their contents; the enclosing selector still decides the scope.
254
+ const TRANSPARENT_AT_RULES = new Set(['media', 'supports', 'layer', 'container', 'scope', 'document']);
255
+
256
+ function stripComments(text) {
257
+ return text.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
258
+ }
259
+
260
+ function scopeOfBlockHeaders(headers) {
261
+ for (let index = headers.length - 1; index >= 0; index -= 1) {
262
+ const header = headers[index];
263
+ if (header.startsWith('@')) {
264
+ const name = header.slice(1).match(/^[\w-]+/);
265
+ if (name && name[0].toLowerCase() === 'theme') return 'root';
266
+ if (name && TRANSPARENT_AT_RULES.has(name[0].toLowerCase())) continue;
267
+ return 'component';
247
268
  }
269
+ return header.split(',').every((selector) => ROOT_SELECTOR.test(selector.trim())) ? 'root' : 'component';
270
+ }
271
+ return 'root';
272
+ }
248
273
 
249
- tokens.push({
250
- token,
251
- value: match[2].trim(),
252
- });
274
+ /**
275
+ * Custom property declarations with the scope they are declared in: `root`
276
+ * for `:root`, `html`, `:host` and `@theme` blocks (through conditional
277
+ * at-rules), `component` for anything else. One pass over the source that
278
+ * skips comments and strings, so a brace in a comment cannot shift the
279
+ * scope; this is the text-side twin of scopeOfNode in scale-inference.js.
280
+ */
281
+ function customPropertyDeclarations(source) {
282
+ const declarations = [];
283
+ const headers = [];
284
+ let headerStart = 0;
285
+ let index = 0;
286
+ const length = source.length;
287
+
288
+ while (index < length) {
289
+ const char = source[index];
290
+ const next = source[index + 1];
291
+
292
+ if (char === '/' && next === '*') {
293
+ const end = source.indexOf('*/', index + 2);
294
+ index = end === -1 ? length : end + 2;
295
+ continue;
296
+ }
297
+ if (char === '/' && next === '/') {
298
+ const end = source.indexOf('\n', index);
299
+ index = end === -1 ? length : end;
300
+ continue;
301
+ }
302
+ if (char === '"' || char === "'") {
303
+ let end = index + 1;
304
+ while (end < length && source[end] !== char) end += source[end] === '\\' ? 2 : 1;
305
+ index = end + 1;
306
+ continue;
307
+ }
308
+ if (char === '{') {
309
+ if (source[index - 1] === '#') {
310
+ const end = source.indexOf('}', index);
311
+ index = end === -1 ? length : end + 1;
312
+ continue;
313
+ }
314
+ headers.push(stripComments(source.slice(headerStart, index)).trim().replace(/\s+/g, ' '));
315
+ headerStart = index + 1;
316
+ index += 1;
317
+ continue;
318
+ }
319
+ if (char === '}') {
320
+ headers.pop();
321
+ headerStart = index + 1;
322
+ index += 1;
323
+ continue;
324
+ }
325
+ if (char === ';') {
326
+ headerStart = index + 1;
327
+ index += 1;
328
+ continue;
329
+ }
330
+ if (char === '-' && next === '-' && !/[\w-]/.test(source[index - 1] || '')) {
331
+ const match = /^(--[\w-]+)\s*:\s*([^;{}]+)/.exec(source.slice(index));
332
+ if (match) {
333
+ declarations.push({ scope: scopeOfBlockHeaders(headers), token: match[1], value: match[2].trim() });
334
+ index += match[0].length;
335
+ continue;
336
+ }
337
+ }
338
+ index += 1;
253
339
  }
254
340
 
255
- tokens.push(...collectScssTokens(source, matchesKind));
256
- return tokens;
341
+ return declarations;
257
342
  }
258
343
 
259
344
  /**
@@ -283,16 +368,40 @@ function collectScssTokens(source, matchesKind) {
283
368
  };
284
369
 
285
370
  const tokens = [];
286
- const push = (tokenName, value) => {
287
- if (!value || !matchesKind(tokenName)) {
371
+ const push = (tokenName, value, force = false) => {
372
+ if (!value || (!force && !matchesKind(tokenName))) {
373
+ return;
374
+ }
375
+ const formatted = formatScssValue(value);
376
+ // A unitless number is a multiplier or a map index, not a length. Zero is fine.
377
+ if (/^-?\d*\.?\d+$/.test(formatted) && parseFloat(formatted) !== 0) {
288
378
  return;
289
379
  }
290
- tokens.push({ token: tokenName, value: formatScssValue(value) });
380
+ tokens.push({ token: tokenName, value: formatted });
291
381
  };
382
+ // Namespaced maps such as GOV.UK's $govuk-spacing-points are only accepted
383
+ // for the spacing kind, and only when they hold a real ladder (issue #86).
384
+ const spacingKind = matchesKind('$spacing-probe');
292
385
 
293
386
  for (const [name, raw] of declarations) {
294
387
  const tokenName = `$${name}`;
295
388
  if (isScssMap(raw)) {
389
+ if (!matchesKind(tokenName) && spacingKind && NAMESPACED_SPACING_MAP.test(tokenName)) {
390
+ const entries = [];
391
+ walkScssMap(raw, [tokenName], (pathName, expression) => {
392
+ const value = evaluateScssExpression(expression, resolveVariable, new Set());
393
+ if (value) {
394
+ entries.push([pathName, value]);
395
+ }
396
+ });
397
+ const distinct = new Set(entries.map(([, value]) => formatScssValue(value)).filter((value) => /^-?\d*\.?\d+(?:px|rem|em)$/.test(value) && parseFloat(value) !== 0));
398
+ if (distinct.size >= MIN_NAMESPACED_MAP_LENGTHS) {
399
+ for (const [pathName, value] of entries) {
400
+ push(pathName, value, true);
401
+ }
402
+ }
403
+ continue;
404
+ }
296
405
  walkScssMap(raw, [tokenName], (pathName, expression) => {
297
406
  push(pathName, evaluateScssExpression(expression, resolveVariable, new Set()));
298
407
  });
@@ -304,6 +413,10 @@ function collectScssTokens(source, matchesKind) {
304
413
  return tokens;
305
414
  }
306
415
 
416
+ /** `$<namespace>-spacing-points`, `$<ns>-space-scale`: one namespace segment before the anchor. */
417
+ const NAMESPACED_SPACING_MAP = /^\$[a-z0-9]+-(?:space|spacing|spacer)s?(?:-|$)/i;
418
+ const MIN_NAMESPACED_MAP_LENGTHS = 4;
419
+
307
420
  function stripScssComments(source) {
308
421
  return source
309
422
  .replace(/\/\*[\s\S]*?\*\//g, '')
@@ -381,7 +494,16 @@ function splitTopLevel(text, separator) {
381
494
  return parts;
382
495
  }
383
496
 
497
+ // Real token maps nest two or three levels; generated or adversarial input can
498
+ // nest without bound. Past these limits the value is treated like any other
499
+ // expression the evaluator cannot handle: skipped, never guessed, never thrown.
500
+ const MAX_SCSS_MAP_DEPTH = 16;
501
+ const MAX_SCSS_EXPRESSION_DEPTH = 64;
502
+
384
503
  function walkScssMap(raw, pathSegments, visit) {
504
+ if (pathSegments.length > MAX_SCSS_MAP_DEPTH) {
505
+ return;
506
+ }
385
507
  const inner = raw.slice(1, -1);
386
508
  for (const entry of splitTopLevel(inner, ',')) {
387
509
  const pair = splitTopLevel(entry, ':');
@@ -454,7 +576,18 @@ function evaluateScssExpression(expression, resolveVariable, stack) {
454
576
  return { number: op === '+' ? left.number + right.number : left.number - right.number, unit: left.unit || right.unit };
455
577
  };
456
578
 
579
+ let depth = 0;
457
580
  const parsePrimary = () => {
581
+ if (depth > MAX_SCSS_EXPRESSION_DEPTH) return null;
582
+ depth += 1;
583
+ try {
584
+ return parsePrimaryInner();
585
+ } finally {
586
+ depth -= 1;
587
+ }
588
+ };
589
+
590
+ const parsePrimaryInner = () => {
458
591
  const token = next();
459
592
  if (!token) return null;
460
593
  if (token.type === 'number') return parseNumber(token.raw);
@@ -633,6 +766,7 @@ function extractTokenName(value) {
633
766
  function addDefinition(definitions, {
634
767
  baseFontSize,
635
768
  file,
769
+ scope = 'root',
636
770
  source,
637
771
  token,
638
772
  value,
@@ -640,12 +774,14 @@ function addDefinition(definitions, {
640
774
  const entry = definitions.get(token) || {
641
775
  files: new Set(),
642
776
  normalizedValues: new Set(),
777
+ scopes: new Set(),
643
778
  sources: new Set(),
644
779
  token,
645
780
  values: new Set(),
646
781
  };
647
782
 
648
783
  entry.files.add(file);
784
+ entry.scopes.add(scope);
649
785
  entry.sources.add(source);
650
786
  entry.values.add(String(value).trim());
651
787
 
@@ -719,7 +855,9 @@ module.exports = {
719
855
  VALID_TOKEN_KINDS,
720
856
  VALID_TOKEN_SOURCE_FORMATS,
721
857
  addDefinition,
858
+ collectCssTokens,
722
859
  collectScssTokens,
860
+ customPropertyDeclarations,
723
861
  createTokenKindMatcher,
724
862
  getNormalizedValueKeys,
725
863
  normalizeTokenKind,
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const valueParser = require('postcss-value-parser');
4
- const stylelint = require('stylelint');
5
4
  const {
6
5
  MATH_FUNCTIONS,
7
6
  TRANSLATE_FUNCTIONS,
8
- } = require('./constants');
7
+ } = require('./css-vocabulary');
9
8
 
10
9
  function propertyMatches(prop, patterns) {
11
10
  const normalized = prop.toLowerCase();
@@ -26,20 +25,6 @@ function isKeyword(value, ignoreValues) {
26
25
  return ignoreValues.includes(String(value).toLowerCase());
27
26
  }
28
27
 
29
- function createTokenRegex(tokenPattern, result, ruleName) {
30
- try {
31
- return new RegExp(tokenPattern);
32
- } catch {
33
- stylelint.utils.report({
34
- message: `Invalid tokenPattern regex: ${tokenPattern}`,
35
- result,
36
- ruleName,
37
- });
38
-
39
- return /^--space-/;
40
- }
41
- }
42
-
43
28
  function isTokenFunction(node, tokenFunctions, tokenRegex) {
44
29
  if (node.type !== 'function') {
45
30
  return false;
@@ -213,7 +198,6 @@ function declarationValueIndex(decl) {
213
198
  }
214
199
 
215
200
  module.exports = {
216
- createTokenRegex,
217
201
  declarationValueIndex,
218
202
  isKeyword,
219
203
  isMathFunction,
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { formatTime } = require('../../utils/time');
4
- const { createTailwindMotionAnalyzer } = require('../../utils/tailwind-motion-analysis');
3
+ const { formatTime } = require('../../core/time');
4
+ const { createTailwindMotionAnalyzer } = require('../../core/tailwind-motion-analysis');
5
5
 
6
6
  const RULE_NAME = 'tailwind-class-use-motion-scale';
7
7
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { formatLength } = require('../../utils/length');
4
- const { createTailwindClassAnalyzer } = require('../../utils/tailwind-class-analysis');
3
+ const { formatLength } = require('../../core/length');
4
+ const { createTailwindClassAnalyzer } = require('../../core/tailwind-class-analysis');
5
5
 
6
6
  const RULE_NAME = 'tailwind-class-use-scale';
7
7
 
@@ -3,33 +3,32 @@
3
3
  const stylelint = require('stylelint');
4
4
  const valueParser = require('postcss-value-parser');
5
5
  const {
6
+ fixedLengthValue,
6
7
  formatLength,
7
- fromPx,
8
8
  isHairlineLength,
9
9
  nearestScaleValues,
10
- normalizeScale,
11
- normalizeScaleByUnit,
12
10
  numbersEqual,
13
11
  parseLengthToken,
14
12
  toPx,
15
- } = require('../../utils/length');
13
+ } = require('../../core/length');
16
14
  const {
17
15
  buildScaleOptions,
18
- resolvePropertyScale,
19
- validateNoOffscaleTransformSecondaryOptions,
20
- } = require('../../utils/options');
16
+ createPropertyScaleResolver,
17
+ } = require('../../core/options');
21
18
  const {
22
- declarationValueIndex,
23
19
  isMathFunction,
24
20
  shouldLintMathArgument,
25
21
  walkRootValueNodes,
26
22
  walkTransformTranslateNodes,
27
- } = require('../../utils/value-utils');
23
+ } = require('../../core/value-nodes');
28
24
 
29
25
  const {
30
- DEFAULT_AUTO_TOKEN_PATTERN,
31
- resolveAutoScale,
32
- } = require('../../utils/scale-inference');
26
+ withResolvedScale,
27
+ } = require('../../core/scale-inference');
28
+
29
+ const { validatePrimary, validateNoOffscaleTransformSecondaryOptions } = require('../validate');
30
+
31
+ const { reportInvalidPreset, reportValueNode } = require('../report');
33
32
 
34
33
  const ruleName = 'rhythmguard/no-offscale-transform';
35
34
  const messages = stylelint.utils.ruleMessages(ruleName, {
@@ -39,33 +38,9 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
39
38
  `Unexpected transform translation value "${value}". Use scale values (nearest: ${lower} or ${upper}).`,
40
39
  });
41
40
 
42
- function getFixedNodeValue(parsedLength, nearestPx, options) {
43
- const unit = parsedLength.unit || 'px';
44
- if (unit === '%' || !options.units.includes(unit)) {
45
- return null;
46
- }
47
-
48
- const signedNearest = parsedLength.number < 0 ? -Math.abs(nearestPx) : nearestPx;
49
-
50
- if (options.unitStrategy === 'exact') {
51
- return formatLength(signedNearest, parsedLength.unit || 'px');
52
- }
53
-
54
- const converted = fromPx(signedNearest, unit, options.baseFontSize);
55
-
56
- if (converted === null) {
57
- return null;
58
- }
59
-
60
- return formatLength(converted, unit);
61
- }
62
-
63
41
  const ruleFunction = (primary, secondaryOptions) => {
64
42
  return (root, result) => {
65
- const valid = stylelint.utils.validateOptions(result, ruleName, {
66
- actual: primary,
67
- possible: [true],
68
- });
43
+ const valid = validatePrimary(result, ruleName, primary);
69
44
 
70
45
  if (!valid) {
71
46
  return;
@@ -81,43 +56,11 @@ const ruleFunction = (primary, secondaryOptions) => {
81
56
  }
82
57
 
83
58
  const options = buildScaleOptions(secondaryOptions);
84
- if (options.invalidPreset) {
85
- stylelint.utils.report({
86
- message: messages.invalidPreset(options.invalidPreset, options.presetNames),
87
- node: root,
88
- result,
89
- ruleName,
90
- });
91
- }
59
+ reportInvalidPreset(options, { message: messages.invalidPreset, result, root, ruleName });
92
60
 
93
- if (options.scaleAuto) {
94
- const inference = resolveAutoScale({
95
- baseFontSize: options.baseFontSize,
96
- root,
97
- scaleSources: options.scaleSources,
98
- tailwindConfigPath: options.tailwindConfigPath,
99
- tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : DEFAULT_AUTO_TOKEN_PATTERN,
100
- });
101
- options.scale = inference.scale;
102
- options.scaleInference = inference;
103
- }
104
-
105
- const scaleCache = new Map();
106
- const getScaleStateForProperty = (prop) => {
107
- const cached = scaleCache.get(prop);
108
- if (cached) {
109
- return cached;
110
- }
111
-
112
- const selectedScale = resolvePropertyScale(prop, options);
113
- const next = {
114
- scaleByUnit: normalizeScaleByUnit(selectedScale),
115
- scalePx: normalizeScale(selectedScale, options.baseFontSize),
116
- };
61
+ withResolvedScale(options, root);
117
62
 
118
- scaleCache.set(prop, next);
119
- return next;
120
- };
63
+ const getScaleStateForProperty = createPropertyScaleResolver(options);
121
64
 
122
65
  root.walkDecls((decl) => {
123
66
  const prop = decl.prop.toLowerCase();
@@ -130,30 +73,18 @@ const ruleFunction = (primary, secondaryOptions) => {
130
73
  let changed = false;
131
74
 
132
75
  const report = (node, nearest, nearestUnit, fixedValue = null) => {
133
- const index = declarationValueIndex(decl) + node.sourceIndex;
134
- const endIndex = index + node.value.length;
135
-
136
- const payload = {
137
- endIndex,
138
- index,
76
+ reportValueNode({
77
+ decl,
139
78
  message: messages.rejected(
140
79
  node.value,
141
80
  formatLength(nearest.lower, nearestUnit),
142
81
  formatLength(nearest.upper, nearestUnit),
143
82
  ),
144
- node: decl,
83
+ node,
84
+ replacement: fixedValue,
145
85
  result,
146
86
  ruleName,
147
- };
148
-
149
- if (fixedValue) {
150
- payload.fix = () => {
151
- node.value = fixedValue;
152
- return true;
153
- };
154
- }
155
-
156
- stylelint.utils.report(payload);
87
+ });
157
88
  };
158
89
 
159
90
  const checkNode = (node) => {
@@ -205,7 +136,7 @@ const ruleFunction = (primary, secondaryOptions) => {
205
136
  }
206
137
 
207
138
  const fixedValue = options.fixToScale
208
- ? getFixedNodeValue(parsedLength, nearest.nearest, options)
139
+ ? fixedLengthValue(parsedLength, nearest.nearest, options)
209
140
  : null;
210
141
 
211
142
  report(node, nearest, unit, fixedValue);
@@ -229,7 +160,7 @@ const ruleFunction = (primary, secondaryOptions) => {
229
160
  }
230
161
 
231
162
  const fixedValue = options.fixToScale
232
- ? getFixedNodeValue(parsedLength, nearest.nearest, options)
163
+ ? fixedLengthValue(parsedLength, nearest.nearest, options)
233
164
  : null;
234
165
 
235
166
  report(node, nearest, 'px', fixedValue);
@@ -5,20 +5,15 @@ const valueParser = require('postcss-value-parser');
5
5
  const {
6
6
  formatLength,
7
7
  isHairlineLength,
8
- normalizeScale,
9
- normalizeScaleByUnit,
10
8
  numbersEqual,
11
9
  parseLengthToken,
12
10
  toPx,
13
- } = require('../../utils/length');
11
+ } = require('../../core/length');
14
12
  const {
15
13
  buildTokenOptions,
16
- resolvePropertyScale,
17
- validatePreferTokenSecondaryOptions,
18
- } = require('../../utils/options');
14
+ createPropertyScaleResolver,
15
+ } = require('../../core/options');
19
16
  const {
20
- createTokenRegex,
21
- declarationValueIndex,
22
17
  isKeyword,
23
18
  isMathFunction,
24
19
  isTokenFunction,
@@ -26,13 +21,15 @@ const {
26
21
  shouldLintMathArgument,
27
22
  walkRootValueNodes,
28
23
  walkTransformTranslateNodes,
29
- } = require('../../utils/value-utils');
30
- const { buildEffectiveTokenMap } = require('../../utils/token-map');
24
+ } = require('../../core/value-nodes');
25
+ const { buildEffectiveTokenMap } = require('../../core/token-map');
31
26
 
32
27
  const {
33
- DEFAULT_AUTO_TOKEN_PATTERN,
34
- resolveAutoScale,
35
- } = require('../../utils/scale-inference');
28
+ withResolvedScale,
29
+ } = require('../../core/scale-inference');
30
+
31
+ const { createTokenRegex, reportInvalidPreset, reportValueNode } = require('../report');
32
+ const { validatePrimary, validatePreferTokenSecondaryOptions } = require('../validate');
36
33
 
37
34
  const ruleName = 'rhythmguard/prefer-token';
38
35
 
@@ -90,10 +87,7 @@ function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
90
87
 
91
88
  const ruleFunction = (primary, secondaryOptions) => {
92
89
  return (root, result) => {
93
- const valid = stylelint.utils.validateOptions(result, ruleName, {
94
- actual: primary,
95
- possible: [true],
96
- });
90
+ const valid = validatePrimary(result, ruleName, primary);
97
91
 
98
92
  if (!valid) {
99
93
  return;
@@ -109,26 +103,9 @@ const ruleFunction = (primary, secondaryOptions) => {
109
103
  }
110
104
 
111
105
  const options = buildTokenOptions(secondaryOptions);
112
- if (options.invalidPreset) {
113
- stylelint.utils.report({
114
- message: messages.invalidPreset(options.invalidPreset, options.presetNames),
115
- node: root,
116
- result,
117
- ruleName,
118
- });
119
- }
106
+ reportInvalidPreset(options, { message: messages.invalidPreset, result, root, ruleName });
120
107
 
121
- if (options.scaleAuto) {
122
- const inference = resolveAutoScale({
123
- baseFontSize: options.baseFontSize,
124
- root,
125
- scaleSources: options.scaleSources,
126
- tailwindConfigPath: options.tailwindConfigPath,
127
- tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : DEFAULT_AUTO_TOKEN_PATTERN,
128
- });
129
- options.scale = inference.scale;
130
- options.scaleInference = inference;
131
- }
108
+ withResolvedScale(options, root);
132
109
 
133
110
  const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
134
111
  const tokenMap = buildEffectiveTokenMap({
@@ -137,23 +114,7 @@ const ruleFunction = (primary, secondaryOptions) => {
137
114
  tokenRegex,
138
115
  });
139
116
 
140
- const scaleCache = new Map();
141
-
142
- const getScaleStateForProperty = (prop) => {
143
- const cached = scaleCache.get(prop);
144
- if (cached) {
145
- return cached;
146
- }
147
-
148
- const selectedScale = resolvePropertyScale(prop, options);
149
- const next = {
150
- scaleByUnit: normalizeScaleByUnit(selectedScale),
151
- scalePx: normalizeScale(selectedScale, options.baseFontSize),
152
- };
153
-
154
- scaleCache.set(prop, next);
155
- return next;
156
- };
117
+ const getScaleStateForProperty = createPropertyScaleResolver(options);
157
118
 
158
119
  root.walkDecls((decl) => {
159
120
  const prop = decl.prop.toLowerCase();
@@ -170,26 +131,7 @@ const ruleFunction = (primary, secondaryOptions) => {
170
131
  let changed = false;
171
132
 
172
133
  const reportNode = (node, replacement = null) => {
173
- const index = declarationValueIndex(decl) + node.sourceIndex;
174
- const endIndex = index + node.value.length;
175
-
176
- const payload = {
177
- endIndex,
178
- index,
179
- message: messages.rejected(node.value),
180
- node: decl,
181
- result,
182
- ruleName,
183
- };
184
-
185
- if (replacement) {
186
- payload.fix = () => {
187
- node.value = replacement;
188
- return true;
189
- };
190
- }
191
-
192
- stylelint.utils.report(payload);
134
+ reportValueNode({ decl, message: messages.rejected(node.value), node, replacement, result, ruleName });
193
135
  };
194
136
 
195
137
  const checkWordNode = (node, context) => {