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 +37 -0
- package/README.md +45 -12
- package/assets/rhythmguard-campaign-60s.gif +0 -0
- package/assets/rhythmguard-campaign-60s.webm +0 -0
- package/package.json +2 -1
- package/src/configs/strict.js +12 -0
- package/src/rules/no-offscale-transform/index.js +17 -1
- package/src/rules/prefer-token/index.js +21 -4
- package/src/rules/use-scale/index.js +17 -1
- package/src/utils/options.js +301 -0
- package/src/utils/value-utils.js +10 -1
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`](
|
|
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`](
|
|
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`](
|
|
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`](
|
|
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.
|
|
441
|
-
5. `
|
|
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
|
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "1.
|
|
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": {
|
package/src/configs/strict.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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({
|
package/src/utils/options.js
CHANGED
|
@@ -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
|
};
|
package/src/utils/value-utils.js
CHANGED
|
@@ -50,7 +50,16 @@ function isTokenFunction(node, tokenFunctions, tokenRegex) {
|
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const
|
|
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
|
|