stylelint-plugin-rhythmguard 1.2.0 → 1.3.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 CHANGED
@@ -6,6 +6,43 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-02-17
10
+
11
+ ### Added
12
+
13
+ - Strict `secondaryOptions` validation for all three rules:
14
+ - `rhythmguard/use-scale`
15
+ - `rhythmguard/prefer-token`
16
+ - `rhythmguard/no-offscale-transform`
17
+ - Invalid option names (for example `sevverity`) now fail with Stylelint invalid option warnings instead of silently being ignored.
18
+ - Type/shape validation for option payloads (for example `properties` must be an array, `tokenMap` must be an object).
19
+ - Regression tests for invalid secondary option names and option value shapes.
20
+
21
+ ### Changed
22
+
23
+ - Added `known-css-properties` as a direct runtime dependency to guarantee `properties` option validation in consumer installs.
24
+ - `properties` option validation now checks supported spacing property names against known CSS property metadata (plus `translate-x`, `translate-y`, `translate-z`).
25
+
26
+ ## [1.2.1] - 2026-02-17
27
+
28
+ ### Fixed
29
+
30
+ - Ignore invalid unitless non-zero literals (`margin: 13`) across all rules instead of treating them like `px` and autofixing them.
31
+ - Reduced strict-mode transform overlap by scoping `rhythmguard/use-scale` away from transform properties in the shared strict config.
32
+ - `rhythmguard/prefer-token` now supports `enforceInsideMathFunctions` for optional math-function enforcement.
33
+ - Hardened `var()` token argument detection to parse the first argument structurally (rather than comma string splitting).
34
+ - npm README link integrity: docs links now resolve to absolute GitHub URLs from the npm package page.
35
+ - Release workflow now detects missing `NPM_TOKEN` and skips publish cleanly with an explicit notice instead of failing.
36
+
37
+ ### Added
38
+
39
+ - Dev.to article link in README resources:
40
+ - https://dev.to/petrilahdelma/enforcing-your-spacing-standards-with-rhythmguard-a-custom-stylelint-plugin-1ojj
41
+ - Regression tests covering:
42
+ - unitless non-zero handling in all three rules
43
+ - strict transform overlap guard
44
+ - prefer-token math-function enforcement toggle
45
+
9
46
  ## [1.2.0] - 2026-02-17
10
47
 
11
48
  ### Added
package/README.md CHANGED
@@ -14,6 +14,16 @@ High-precision spacing governance for CSS and design systems.
14
14
 
15
15
  `stylelint-plugin-rhythmguard` enforces spacing discipline across margin, padding, gap, inset, scroll spacing, and translate motion offsets.
16
16
 
17
+ ## 60-second Demo
18
+
19
+ <p align="center">
20
+ <a href="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.webm">
21
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second campaign demo" />
22
+ </a>
23
+ </p>
24
+
25
+ - Full video (WebM): [rhythmguard-campaign-60s.webm](https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.webm)
26
+
17
27
  I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
18
28
 
19
29
  It is built for teams that want:
@@ -47,7 +57,6 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
47
57
 
48
58
  ```json
49
59
  {
50
- "plugins": ["stylelint-plugin-rhythmguard"],
51
60
  "extends": ["stylelint-plugin-rhythmguard/configs/recommended"]
52
61
  }
53
62
  ```
@@ -56,16 +65,16 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
56
65
 
57
66
  ```json
58
67
  {
59
- "plugins": ["stylelint-plugin-rhythmguard"],
60
68
  "extends": ["stylelint-plugin-rhythmguard/configs/strict"]
61
69
  }
62
70
  ```
63
71
 
72
+ `strict` intentionally delegates transform translation enforcement to `rhythmguard/no-offscale-transform` to reduce overlapping warnings from `use-scale`.
73
+
64
74
  ### Tailwind config
65
75
 
66
76
  ```json
67
77
  {
68
- "plugins": ["stylelint-plugin-rhythmguard"],
69
78
  "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
70
79
  }
71
80
  ```
@@ -147,6 +156,24 @@ Scale resolution precedence:
147
156
  3. `preset`
148
157
  4. default `rhythmic-4` scale
149
158
 
159
+ ## Option Validation
160
+
161
+ Rhythmguard validates `secondaryOptions` for each rule before linting declarations.
162
+
163
+ - Unknown option names fail fast with Stylelint invalid option warnings.
164
+ - Invalid option shapes fail fast (for example string vs array mismatches).
165
+ - `properties` string entries are validated against supported CSS spacing property names, plus `translate-x`, `translate-y`, and `translate-z`.
166
+
167
+ Example typo that now fails immediately:
168
+
169
+ ```json
170
+ {
171
+ "rules": {
172
+ "rhythmguard/use-scale": [true, { "sevverity": "warning" }]
173
+ }
174
+ }
175
+ ```
176
+
150
177
  ## Built-in Scale Presets
151
178
 
152
179
  | Preset | Pattern | Scale |
@@ -191,7 +218,7 @@ Aliases:
191
218
  - Product presets are based on widely-used design-system spacing frameworks.
192
219
  - Editorial presets model baseline-grid cadence used in long-form typography and column layouts.
193
220
  - Theory presets expose mathematically-derived modular scales from design theory and typographic proportion systems.
194
- - Full research notes and sources are documented in [`docs/SCALE_RESEARCH.md`](./docs/SCALE_RESEARCH.md).
221
+ - Full research notes and sources are documented in [`docs/SCALE_RESEARCH.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/SCALE_RESEARCH.md).
195
222
 
196
223
  ## Community Scale Registry
197
224
 
@@ -219,7 +246,7 @@ npm run scales:validate
219
246
 
220
247
  3. Open a PR with your scale JSON.
221
248
 
222
- Full specification and policy: [`docs/COMMUNITY_SCALES.md`](./docs/COMMUNITY_SCALES.md).
249
+ Full specification and policy: [`docs/COMMUNITY_SCALES.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/COMMUNITY_SCALES.md).
223
250
 
224
251
  If your scale is private or very niche, keep it in your project config with `customScale` instead of contributing it to the shared registry.
225
252
 
@@ -268,7 +295,7 @@ Options:
268
295
  | `allowPercentages` | `boolean` | `true` | Allows `%` values without scale checks |
269
296
  | `fixToScale` | `boolean` | `true` | Enables nearest-value autofix |
270
297
  | `enforceInsideMathFunctions` | `boolean` | `false` | Lints `calc()/clamp()/min()/max()` internals |
271
- | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set |
298
+ | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set; string values must be supported spacing property names |
272
299
 
273
300
  ### `rhythmguard/prefer-token`
274
301
 
@@ -301,9 +328,10 @@ Options:
301
328
  | `customScale` | `Array<number|string>` | `undefined` | Highest-priority custom scale override |
302
329
  | `scale` | `Array<number|string>` | `[0,4,8,12,16,24,32,40,48,64]` | Used when `allowNumericScale` is enabled |
303
330
  | `baseFontSize` | `number` | `16` | Used for scale checks with `rem`/`em` |
331
+ | `enforceInsideMathFunctions` | `boolean` | `false` | Lints `calc()/clamp()/min()/max()` internals |
304
332
  | `tokenMap` | `Record<string,string>` | `{}` | Enables autofix from raw value to token |
305
333
  | `ignoreValues` | `string[]` | CSS global keywords + `auto` | Skips keyword literals |
306
- | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set |
334
+ | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set; string values must be supported spacing property names |
307
335
 
308
336
  ### `rhythmguard/no-offscale-transform`
309
337
 
@@ -325,7 +353,7 @@ Example:
325
353
 
326
354
  Options:
327
355
 
328
- `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties.
356
+ `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties. Its secondary options are also validated for unknown keys and invalid value shapes.
329
357
 
330
358
  ## Tailwind CSS Integration
331
359
 
@@ -365,7 +393,7 @@ Then pair with:
365
393
  - `eslint-plugin-tailwindcss` for class-string rules (including arbitrary-value governance).
366
394
  - `prettier-plugin-tailwindcss` for deterministic class ordering.
367
395
 
368
- Detailed setup reference: [`docs/TAILWIND.md`](./docs/TAILWIND.md).
396
+ Detailed setup reference: [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/TAILWIND.md).
369
397
 
370
398
  ### Tailwind token function support
371
399
 
@@ -430,15 +458,20 @@ Benchmark with autofix enabled:
430
458
  npm run bench:perf:fix
431
459
  ```
432
460
 
433
- Detailed methodology and custom args are documented in [`docs/BENCHMARKING.md`](./docs/BENCHMARKING.md).
461
+ Detailed methodology and custom args are documented in [`docs/BENCHMARKING.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/BENCHMARKING.md).
462
+
463
+ ## Article
464
+
465
+ - Dev.to: [Enforcing your spacing standards with Rhythmguard](https://dev.to/petrilahdelma/enforcing-your-spacing-standards-with-rhythmguard-a-custom-stylelint-plugin-1ojj)
434
466
 
435
467
  ## Release Workflow
436
468
 
437
469
  1. Create a GitHub release.
438
470
  2. `release.yml` runs the Node/Stylelint matrix validation.
439
471
  3. A tarball smoke test validates package exports and install behavior.
440
- 4. The package is published to npm with provenance (`npm publish --provenance`).
441
- 5. `post-publish-smoke.yml` verifies the published npm version can be installed and run in a clean project.
472
+ 4. If `NPM_TOKEN` is configured in repository secrets, the package is published to npm with provenance (`npm publish --provenance`).
473
+ 5. If `NPM_TOKEN` is not configured, publish is skipped with an explicit workflow notice.
474
+ 6. `post-publish-smoke.yml` verifies the published npm version can be installed and run in a clean project (and skips cleanly if the version is not on npm).
442
475
 
443
476
  ## Support and Bug Reports
444
477
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Stylelint plugin for spacing scale and token enforcement",
5
5
  "keywords": [
6
6
  "stylelint",
@@ -61,6 +61,7 @@
61
61
  "stylelint": "^16.0.0"
62
62
  },
63
63
  "dependencies": {
64
+ "known-css-properties": "^0.37.0",
64
65
  "stylelint-config-tailwindcss": "^1.0.1"
65
66
  },
66
67
  "devDependencies": {
@@ -6,6 +6,18 @@ module.exports = {
6
6
  'rhythmguard/use-scale': [
7
7
  true,
8
8
  {
9
+ // Keep transform-specific enforcement in no-offscale-transform
10
+ // to avoid duplicate warnings in strict mode.
11
+ properties: [
12
+ /^margin(?:-.+)?$/,
13
+ /^padding(?:-.+)?$/,
14
+ /^gap$/,
15
+ /^row-gap$/,
16
+ /^column-gap$/,
17
+ /^inset(?:-.+)?$/,
18
+ /^scroll-margin(?:-.+)?$/,
19
+ /^scroll-padding(?:-.+)?$/,
20
+ ],
9
21
  scale: [0, 4, 8, 12, 16, 24, 32, 40, 48, 64],
10
22
  },
11
23
  ],
@@ -11,7 +11,10 @@ const {
11
11
  parseLengthToken,
12
12
  toPx,
13
13
  } = require('../../utils/length');
14
- const { buildScaleOptions } = require('../../utils/options');
14
+ const {
15
+ buildScaleOptions,
16
+ validateNoOffscaleTransformSecondaryOptions,
17
+ } = require('../../utils/options');
15
18
  const {
16
19
  declarationValueIndex,
17
20
  isMathFunction,
@@ -54,6 +57,15 @@ const ruleFunction = (primary, secondaryOptions) => {
54
57
  return;
55
58
  }
56
59
 
60
+ const validSecondaryOptions = validateNoOffscaleTransformSecondaryOptions(
61
+ result,
62
+ ruleName,
63
+ secondaryOptions,
64
+ );
65
+ if (!validSecondaryOptions) {
66
+ return;
67
+ }
68
+
57
69
  const options = buildScaleOptions(secondaryOptions);
58
70
  if (options.invalidPreset) {
59
71
  stylelint.utils.report({
@@ -108,6 +120,10 @@ const ruleFunction = (primary, secondaryOptions) => {
108
120
  return;
109
121
  }
110
122
 
123
+ if (parsedLength.unit === '') {
124
+ return;
125
+ }
126
+
111
127
  if (parsedLength.unit === '%' && options.allowPercentages) {
112
128
  return;
113
129
  }
@@ -8,7 +8,10 @@ const {
8
8
  parseLengthToken,
9
9
  toPx,
10
10
  } = require('../../utils/length');
11
- const { buildTokenOptions } = require('../../utils/options');
11
+ const {
12
+ buildTokenOptions,
13
+ validatePreferTokenSecondaryOptions,
14
+ } = require('../../utils/options');
12
15
  const {
13
16
  createTokenRegex,
14
17
  declarationValueIndex,
@@ -53,6 +56,15 @@ const ruleFunction = (primary, secondaryOptions) => {
53
56
  return;
54
57
  }
55
58
 
59
+ const validSecondaryOptions = validatePreferTokenSecondaryOptions(
60
+ result,
61
+ ruleName,
62
+ secondaryOptions,
63
+ );
64
+ if (!validSecondaryOptions) {
65
+ return;
66
+ }
67
+
56
68
  const options = buildTokenOptions(secondaryOptions);
57
69
  if (options.invalidPreset) {
58
70
  stylelint.utils.report({
@@ -109,7 +121,8 @@ const ruleFunction = (primary, secondaryOptions) => {
109
121
 
110
122
  if (
111
123
  parentFunctionName &&
112
- isMathFunction(parentFunctionName)
124
+ isMathFunction(parentFunctionName) &&
125
+ !options.enforceInsideMathFunctions
113
126
  ) {
114
127
  return false;
115
128
  }
@@ -123,6 +136,10 @@ const ruleFunction = (primary, secondaryOptions) => {
123
136
  return false;
124
137
  }
125
138
 
139
+ if (parsedLength.unit === '') {
140
+ return false;
141
+ }
142
+
126
143
  const absPx = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
127
144
  if (absPx === null) {
128
145
  return false;
@@ -148,7 +165,7 @@ const ruleFunction = (primary, secondaryOptions) => {
148
165
  return true;
149
166
  }
150
167
 
151
- if (isMathFunction(node.value)) {
168
+ if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
152
169
  return true;
153
170
  }
154
171
 
@@ -169,7 +186,7 @@ const ruleFunction = (primary, secondaryOptions) => {
169
186
  return true;
170
187
  }
171
188
 
172
- if (isMathFunction(node.value)) {
189
+ if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
173
190
  return true;
174
191
  }
175
192
 
@@ -11,7 +11,10 @@ const {
11
11
  parseLengthToken,
12
12
  toPx,
13
13
  } = require('../../utils/length');
14
- const { buildScaleOptions } = require('../../utils/options');
14
+ const {
15
+ buildScaleOptions,
16
+ validateUseScaleSecondaryOptions,
17
+ } = require('../../utils/options');
15
18
  const {
16
19
  createTokenRegex,
17
20
  declarationValueIndex,
@@ -66,6 +69,10 @@ function checkLengthValue({
66
69
  return false;
67
70
  }
68
71
 
72
+ if (parsedLength.unit === '') {
73
+ return false;
74
+ }
75
+
69
76
  if (parsedLength.unit === '%' && options.allowPercentages) {
70
77
  return false;
71
78
  }
@@ -118,6 +125,15 @@ const ruleFunction = (primary, secondaryOptions) => {
118
125
  return;
119
126
  }
120
127
 
128
+ const validSecondaryOptions = validateUseScaleSecondaryOptions(
129
+ result,
130
+ ruleName,
131
+ secondaryOptions,
132
+ );
133
+ if (!validSecondaryOptions) {
134
+ return;
135
+ }
136
+
121
137
  const options = buildScaleOptions(secondaryOptions);
122
138
  if (options.invalidPreset) {
123
139
  stylelint.utils.report({
@@ -1,9 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ const stylelint = require('stylelint');
4
+ const { all: knownCssProperties = [] } = require('known-css-properties');
3
5
  const {
4
6
  DEFAULT_IGNORE_KEYWORDS,
5
7
  SPACING_PROPERTY_PATTERNS,
6
8
  } = require('./constants');
9
+ const { parseLengthToken } = require('./length');
7
10
  const {
8
11
  getScalePreset,
9
12
  listScalePresetNames,
@@ -11,6 +14,270 @@ const {
11
14
  } = require('../presets/scales');
12
15
 
13
16
  const DEFAULT_SCALE = getScalePreset('rhythmic-4') || [0, 4, 8, 12, 16, 24, 32];
17
+ const VALID_UNITS = new Set(['px', 'rem', 'em']);
18
+ const VALIDATE_ALWAYS = () => true;
19
+
20
+ const supportedSpacingProperties = new Set(
21
+ knownCssProperties.filter((property) =>
22
+ SPACING_PROPERTY_PATTERNS.some((pattern) => pattern.test(property)),
23
+ ),
24
+ );
25
+ supportedSpacingProperties.add('translate-x');
26
+ supportedSpacingProperties.add('translate-y');
27
+ supportedSpacingProperties.add('translate-z');
28
+
29
+ function isPlainObject(value) {
30
+ return (
31
+ value !== null &&
32
+ typeof value === 'object' &&
33
+ !Array.isArray(value)
34
+ );
35
+ }
36
+
37
+ function isBoolean(value) {
38
+ return typeof value === 'boolean';
39
+ }
40
+
41
+ function isFinitePositiveNumber(value) {
42
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
43
+ }
44
+
45
+ function isNonEmptyString(value) {
46
+ return typeof value === 'string' && value.trim().length > 0;
47
+ }
48
+
49
+ function isSupportedUnit(value) {
50
+ if (!isNonEmptyString(value)) {
51
+ return false;
52
+ }
53
+
54
+ return VALID_UNITS.has(value.trim().toLowerCase());
55
+ }
56
+
57
+ function isScaleEntry(value) {
58
+ if (typeof value === 'number') {
59
+ return Number.isFinite(value) && value >= 0;
60
+ }
61
+
62
+ if (!isNonEmptyString(value)) {
63
+ return false;
64
+ }
65
+
66
+ const parsed = parseLengthToken(value);
67
+ if (!parsed || parsed.number < 0) {
68
+ return false;
69
+ }
70
+
71
+ return parsed.unit === '' || VALID_UNITS.has(parsed.unit);
72
+ }
73
+
74
+ function isPropertyPatternEntry(value) {
75
+ if (value instanceof RegExp) {
76
+ return true;
77
+ }
78
+
79
+ if (!isNonEmptyString(value)) {
80
+ return false;
81
+ }
82
+
83
+ const normalized = value.trim().toLowerCase();
84
+ return supportedSpacingProperties.has(normalized);
85
+ }
86
+
87
+ function isTokenMap(value) {
88
+ if (!isPlainObject(value)) {
89
+ return false;
90
+ }
91
+
92
+ return Object.values(value).every((tokenValue) => isNonEmptyString(tokenValue));
93
+ }
94
+
95
+ function validateSecondaryOptionShapes(result, ruleName, secondaryOptions, schema) {
96
+ if (secondaryOptions === undefined || secondaryOptions === null) {
97
+ return true;
98
+ }
99
+
100
+ if (!isPlainObject(secondaryOptions)) {
101
+ return true;
102
+ }
103
+
104
+ let valid = true;
105
+
106
+ for (const [optionName, descriptor] of Object.entries(schema)) {
107
+ const optionValue = secondaryOptions[optionName];
108
+ if (optionValue === undefined) {
109
+ continue;
110
+ }
111
+
112
+ if (descriptor.expectsArray && !Array.isArray(optionValue)) {
113
+ valid = false;
114
+ result.warn(
115
+ `Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
116
+ { stylelintType: 'invalidOption' },
117
+ );
118
+ result.stylelint.stylelintError = true;
119
+ continue;
120
+ }
121
+
122
+ if (descriptor.expectsObject && !isPlainObject(optionValue)) {
123
+ valid = false;
124
+ result.warn(
125
+ `Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
126
+ { stylelintType: 'invalidOption' },
127
+ );
128
+ result.stylelint.stylelintError = true;
129
+ }
130
+ }
131
+
132
+ return valid;
133
+ }
134
+
135
+ function stringifyOptionValue(value) {
136
+ if (typeof value === 'string') {
137
+ return `"${value}"`;
138
+ }
139
+
140
+ return `"${JSON.stringify(value)}"`;
141
+ }
142
+
143
+ function buildPossibleOptionMap(schema) {
144
+ return Object.fromEntries(
145
+ Object.entries(schema).map(([optionName, descriptor]) => [
146
+ optionName,
147
+ [descriptor.entryValidator || VALIDATE_ALWAYS],
148
+ ]),
149
+ );
150
+ }
151
+
152
+ function validateSecondaryOptions({
153
+ result,
154
+ ruleName,
155
+ secondaryOptions,
156
+ schema,
157
+ possibleOptionMap,
158
+ }) {
159
+ const validOptions = stylelint.utils.validateOptions(result, ruleName, {
160
+ actual: secondaryOptions,
161
+ optional: true,
162
+ possible: possibleOptionMap,
163
+ });
164
+ const validShapes = validateSecondaryOptionShapes(
165
+ result,
166
+ ruleName,
167
+ secondaryOptions,
168
+ schema,
169
+ );
170
+
171
+ return validOptions && validShapes;
172
+ }
173
+
174
+ const SCALE_VALIDATION_SCHEMA = Object.freeze({
175
+ allowNegative: Object.freeze({
176
+ entryValidator: isBoolean,
177
+ }),
178
+ allowPercentages: Object.freeze({
179
+ entryValidator: isBoolean,
180
+ }),
181
+ baseFontSize: Object.freeze({
182
+ entryValidator: isFinitePositiveNumber,
183
+ }),
184
+ customScale: Object.freeze({
185
+ entryValidator: isScaleEntry,
186
+ expectsArray: true,
187
+ }),
188
+ enforceInsideMathFunctions: Object.freeze({
189
+ entryValidator: isBoolean,
190
+ }),
191
+ fixToScale: Object.freeze({
192
+ entryValidator: isBoolean,
193
+ }),
194
+ preset: Object.freeze({
195
+ entryValidator: isNonEmptyString,
196
+ }),
197
+ scale: Object.freeze({
198
+ entryValidator: isScaleEntry,
199
+ expectsArray: true,
200
+ }),
201
+ units: Object.freeze({
202
+ entryValidator: isSupportedUnit,
203
+ expectsArray: true,
204
+ }),
205
+ });
206
+
207
+ const USE_SCALE_VALIDATION_SCHEMA = Object.freeze({
208
+ ...SCALE_VALIDATION_SCHEMA,
209
+ ignoreValues: Object.freeze({
210
+ entryValidator: isNonEmptyString,
211
+ expectsArray: true,
212
+ }),
213
+ properties: Object.freeze({
214
+ entryValidator: isPropertyPatternEntry,
215
+ expectsArray: true,
216
+ }),
217
+ tokenFunctions: Object.freeze({
218
+ entryValidator: isNonEmptyString,
219
+ expectsArray: true,
220
+ }),
221
+ tokenPattern: Object.freeze({
222
+ entryValidator: isNonEmptyString,
223
+ }),
224
+ });
225
+
226
+ const NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA = Object.freeze({
227
+ ...SCALE_VALIDATION_SCHEMA,
228
+ });
229
+
230
+ const PREFER_TOKEN_VALIDATION_SCHEMA = Object.freeze({
231
+ allowNumericScale: Object.freeze({
232
+ entryValidator: isBoolean,
233
+ }),
234
+ baseFontSize: Object.freeze({
235
+ entryValidator: isFinitePositiveNumber,
236
+ }),
237
+ customScale: Object.freeze({
238
+ entryValidator: isScaleEntry,
239
+ expectsArray: true,
240
+ }),
241
+ enforceInsideMathFunctions: Object.freeze({
242
+ entryValidator: isBoolean,
243
+ }),
244
+ ignoreValues: Object.freeze({
245
+ entryValidator: isNonEmptyString,
246
+ expectsArray: true,
247
+ }),
248
+ preset: Object.freeze({
249
+ entryValidator: isNonEmptyString,
250
+ }),
251
+ properties: Object.freeze({
252
+ entryValidator: isPropertyPatternEntry,
253
+ expectsArray: true,
254
+ }),
255
+ scale: Object.freeze({
256
+ entryValidator: isScaleEntry,
257
+ expectsArray: true,
258
+ }),
259
+ tokenFunctions: Object.freeze({
260
+ entryValidator: isNonEmptyString,
261
+ expectsArray: true,
262
+ }),
263
+ tokenMap: Object.freeze({
264
+ entryValidator: isTokenMap,
265
+ expectsObject: true,
266
+ }),
267
+ tokenPattern: Object.freeze({
268
+ entryValidator: isNonEmptyString,
269
+ }),
270
+ });
271
+
272
+ const USE_SCALE_POSSIBLE_OPTIONS = Object.freeze(
273
+ buildPossibleOptionMap(USE_SCALE_VALIDATION_SCHEMA),
274
+ );
275
+ const NO_OFFSCALE_TRANSFORM_POSSIBLE_OPTIONS = Object.freeze(
276
+ buildPossibleOptionMap(NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA),
277
+ );
278
+ const PREFER_TOKEN_POSSIBLE_OPTIONS = Object.freeze(
279
+ buildPossibleOptionMap(PREFER_TOKEN_VALIDATION_SCHEMA),
280
+ );
14
281
 
15
282
  function buildScaleOptions(rawOptions) {
16
283
  const options = rawOptions || {};
@@ -62,6 +329,7 @@ function buildTokenOptions(rawOptions) {
62
329
  options.baseFontSize > 0
63
330
  ? options.baseFontSize
64
331
  : 16,
332
+ enforceInsideMathFunctions: options.enforceInsideMathFunctions === true,
65
333
  ignoreValues: Array.isArray(options.ignoreValues)
66
334
  ? options.ignoreValues.map((value) => String(value).toLowerCase())
67
335
  : DEFAULT_IGNORE_KEYWORDS,
@@ -86,7 +354,40 @@ function buildTokenOptions(rawOptions) {
86
354
  };
87
355
  }
88
356
 
357
+ function validateUseScaleSecondaryOptions(result, ruleName, secondaryOptions) {
358
+ return validateSecondaryOptions({
359
+ result,
360
+ ruleName,
361
+ secondaryOptions,
362
+ schema: USE_SCALE_VALIDATION_SCHEMA,
363
+ possibleOptionMap: USE_SCALE_POSSIBLE_OPTIONS,
364
+ });
365
+ }
366
+
367
+ function validateNoOffscaleTransformSecondaryOptions(result, ruleName, secondaryOptions) {
368
+ return validateSecondaryOptions({
369
+ result,
370
+ ruleName,
371
+ secondaryOptions,
372
+ schema: NO_OFFSCALE_TRANSFORM_VALIDATION_SCHEMA,
373
+ possibleOptionMap: NO_OFFSCALE_TRANSFORM_POSSIBLE_OPTIONS,
374
+ });
375
+ }
376
+
377
+ function validatePreferTokenSecondaryOptions(result, ruleName, secondaryOptions) {
378
+ return validateSecondaryOptions({
379
+ result,
380
+ ruleName,
381
+ secondaryOptions,
382
+ schema: PREFER_TOKEN_VALIDATION_SCHEMA,
383
+ possibleOptionMap: PREFER_TOKEN_POSSIBLE_OPTIONS,
384
+ });
385
+ }
386
+
89
387
  module.exports = {
90
388
  buildScaleOptions,
91
389
  buildTokenOptions,
390
+ validateNoOffscaleTransformSecondaryOptions,
391
+ validatePreferTokenSecondaryOptions,
392
+ validateUseScaleSecondaryOptions,
92
393
  };
@@ -50,7 +50,16 @@ function isTokenFunction(node, tokenFunctions, tokenRegex) {
50
50
  return true;
51
51
  }
52
52
 
53
- const firstArg = valueParser.stringify(node.nodes).split(',')[0].trim();
53
+ const firstArgNodes = [];
54
+ for (const child of node.nodes) {
55
+ if (child.type === 'div' && child.value === ',') {
56
+ break;
57
+ }
58
+
59
+ firstArgNodes.push(child);
60
+ }
61
+
62
+ const firstArg = valueParser.stringify(firstArgNodes).trim();
54
63
  return tokenRegex.test(firstArg);
55
64
  }
56
65