stylelint-plugin-rhythmguard 3.8.0 → 3.10.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 +46 -0
- package/CONTRIBUTING.md +11 -6
- package/README.md +16 -9
- package/package.json +7 -7
- package/src/audit/args.js +2 -0
- package/src/audit/config.js +24 -1
- package/src/audit/contract.js +3 -1
- package/src/audit/render-markdown.js +35 -0
- package/src/audit/render-text.js +6 -0
- package/src/audit/report.js +15 -5
- package/src/audit/scan/files.js +2 -2
- package/src/audit/scan/stylesheets.js +14 -2
- package/src/audit/scan/templates.js +45 -11
- package/src/audit/shared.js +3 -0
- package/src/audit/token-ladders.js +216 -0
- package/src/configs/embed.js +2 -2
- package/src/core/css-vocabulary.js +26 -0
- package/src/core/options.js +78 -8
- package/src/core/scale-inference.js +55 -12
- package/src/core/token-index.js +60 -12
- package/src/core/token-packages.json +7 -0
- package/src/core/token-sources.js +2 -2
- package/src/core/value-nodes.js +51 -0
- package/src/rules/no-offscale-transform/index.js +9 -3
- package/src/rules/prefer-token/index.js +93 -12
- package/src/rules/use-scale/index.js +41 -8
- package/types/audit.d.ts +6 -0
- package/types/shared.d.ts +8 -0
|
@@ -123,7 +123,7 @@ function parseTokenSources({
|
|
|
123
123
|
continue;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
let tokens
|
|
126
|
+
let tokens;
|
|
127
127
|
const matchesSource = normalizedSource.tokenPattern
|
|
128
128
|
? createPatternMatcher(normalizedSource.tokenPattern, matchesKind)
|
|
129
129
|
: matchesKind;
|
|
@@ -428,7 +428,7 @@ function createScssVariableResolver(source, fallback = () => null) {
|
|
|
428
428
|
if (stack.has(name)) {
|
|
429
429
|
return null;
|
|
430
430
|
}
|
|
431
|
-
let value
|
|
431
|
+
let value;
|
|
432
432
|
if (declarations.has(name)) {
|
|
433
433
|
stack.add(name);
|
|
434
434
|
const raw = declarations.get(name);
|
package/src/core/value-nodes.js
CHANGED
|
@@ -191,6 +191,55 @@ function shouldLintMathArgument(context, options) {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
const BOUND_FUNCTIONS = new Set(['clamp', 'min', 'max']);
|
|
195
|
+
|
|
196
|
+
function isBoundFunction(node) {
|
|
197
|
+
return node.type === 'function' && BOUND_FUNCTIONS.has(node.value.toLowerCase());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function splitArguments(nodes) {
|
|
201
|
+
const args = [[]];
|
|
202
|
+
for (const node of nodes) {
|
|
203
|
+
if (node.type === 'div' && node.value === ',') {
|
|
204
|
+
args.push([]);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (node.type !== 'space' && node.type !== 'comment') {
|
|
208
|
+
args[args.length - 1].push(node);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return args;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The static bounds of a fluid value (issue #203): the minimum and maximum of
|
|
216
|
+
* `clamp(MIN, VAL, MAX)` and every argument of `min()` and `max()`. A bound
|
|
217
|
+
* counts only when it is one word, a length literal; the preferred value of
|
|
218
|
+
* clamp(), an expression, or a var() is fluid by design and left alone. A
|
|
219
|
+
* bound that is itself min(), max() or clamp() is followed.
|
|
220
|
+
*/
|
|
221
|
+
function collectMathBounds(fnNode) {
|
|
222
|
+
const name = fnNode.value.toLowerCase();
|
|
223
|
+
const args = splitArguments(fnNode.nodes);
|
|
224
|
+
const positioned = name === 'clamp'
|
|
225
|
+
? (args.length === 3 ? [[args[0], 'clamp() minimum'], [args[2], 'clamp() maximum']] : [])
|
|
226
|
+
: args.map((arg) => [arg, `${name}() argument`]);
|
|
227
|
+
|
|
228
|
+
const bounds = [];
|
|
229
|
+
for (const [arg, position] of positioned) {
|
|
230
|
+
if (arg.length !== 1) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const [node] = arg;
|
|
234
|
+
if (node.type === 'word') {
|
|
235
|
+
bounds.push({ node, position });
|
|
236
|
+
} else if (isBoundFunction(node)) {
|
|
237
|
+
bounds.push(...collectMathBounds(node));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return bounds;
|
|
241
|
+
}
|
|
242
|
+
|
|
194
243
|
function declarationValueIndex(decl) {
|
|
195
244
|
const declarationText = decl.toString();
|
|
196
245
|
const idx = declarationText.indexOf(decl.value);
|
|
@@ -198,7 +247,9 @@ function declarationValueIndex(decl) {
|
|
|
198
247
|
}
|
|
199
248
|
|
|
200
249
|
module.exports = {
|
|
250
|
+
collectMathBounds,
|
|
201
251
|
declarationValueIndex,
|
|
252
|
+
isBoundFunction,
|
|
202
253
|
isKeyword,
|
|
203
254
|
isMathFunction,
|
|
204
255
|
isTokenFunction,
|
|
@@ -69,7 +69,13 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
69
69
|
withResolvedScale(options, root);
|
|
70
70
|
if (options.fixWith === 'token') {
|
|
71
71
|
options.tokenIndex = tokenIndexFromDefinitions(
|
|
72
|
-
collectTokenDefinitions({
|
|
72
|
+
collectTokenDefinitions({
|
|
73
|
+
baseFontSize: options.baseFontSize,
|
|
74
|
+
root,
|
|
75
|
+
scaleSources: options.scaleSources,
|
|
76
|
+
tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : null,
|
|
77
|
+
tokenRegex: new RegExp(options.tokenPattern),
|
|
78
|
+
}),
|
|
73
79
|
options.baseFontSize,
|
|
74
80
|
);
|
|
75
81
|
}
|
|
@@ -157,7 +163,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
const fixedValue = options.fixToScale
|
|
160
|
-
? replacementFor(parsedLength, nearest.nearest, options)
|
|
166
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
161
167
|
: null;
|
|
162
168
|
|
|
163
169
|
report(node, nearest, unit, fixedValue);
|
|
@@ -181,7 +187,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
const fixedValue = options.fixToScale
|
|
184
|
-
? replacementFor(parsedLength, nearest.nearest, options)
|
|
190
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
185
191
|
: null;
|
|
186
192
|
|
|
187
193
|
report(node, nearest, 'px', fixedValue);
|
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
createPropertyScaleResolver,
|
|
15
15
|
} = require('../../core/options');
|
|
16
16
|
const {
|
|
17
|
+
collectMathBounds,
|
|
18
|
+
isBoundFunction,
|
|
17
19
|
isKeyword,
|
|
18
20
|
isMathFunction,
|
|
19
21
|
isTokenFunction,
|
|
@@ -25,25 +27,40 @@ const {
|
|
|
25
27
|
const { buildEffectiveTokenMap } = require('../../core/token-map');
|
|
26
28
|
|
|
27
29
|
const {
|
|
30
|
+
collectTokenDefinitions,
|
|
28
31
|
withResolvedScale,
|
|
29
32
|
} = require('../../core/scale-inference');
|
|
30
33
|
|
|
31
34
|
const { createTokenRegex, reportInvalidPreset, reportValueNode } = require('../report');
|
|
32
35
|
const { validatePrimary, validatePreferTokenSecondaryOptions } = require('../validate');
|
|
33
36
|
|
|
34
|
-
const {
|
|
37
|
+
const {
|
|
38
|
+
negateReplacement,
|
|
39
|
+
tokenFitsProperty,
|
|
40
|
+
tokenIndexFromDefinitions,
|
|
41
|
+
tokensForLength,
|
|
42
|
+
} = require('../../core/token-index');
|
|
35
43
|
|
|
36
44
|
const ruleName = 'rhythmguard/prefer-token';
|
|
37
45
|
|
|
38
46
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
39
47
|
invalidPreset: (presetName, presetNames) =>
|
|
40
48
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
41
|
-
rejected: (value, replacement = null, origin = null, note = '') =>
|
|
42
|
-
`Unexpected raw scale value "${value}". ${replacement
|
|
49
|
+
rejected: (value, replacement = null, origin = null, note = '', candidates = [], position = null) =>
|
|
50
|
+
`Unexpected raw ${position || 'scale value'} "${value}". ${replacement
|
|
43
51
|
? `Use ${replacement}${origin ? ` (${origin})` : ''}.`
|
|
44
|
-
:
|
|
52
|
+
: candidates.length > 1
|
|
53
|
+
? `Use one of the tokens that hold it: ${listTokens(candidates)}.`
|
|
54
|
+
: 'No known token holds this value; use the nearest token or add one.'}${note ? ` ${note}` : ''}`,
|
|
45
55
|
});
|
|
46
56
|
|
|
57
|
+
function listTokens(tokens) {
|
|
58
|
+
const references = tokens.map((token) => `var(${token})`);
|
|
59
|
+
return references.length > 1
|
|
60
|
+
? `${references.slice(0, -1).join(', ')} or ${references[references.length - 1]}`
|
|
61
|
+
: references.join('');
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
function applyNegativeToken(replacement, parsedLength) {
|
|
48
65
|
return !replacement || parsedLength.number >= 0 ? replacement : negateReplacement(replacement);
|
|
49
66
|
}
|
|
@@ -74,6 +91,36 @@ function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
|
74
91
|
return null;
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The tokens use-scale's `fixWith: "token"` would see: the stylesheet,
|
|
96
|
+
* `scaleSources`, `.rhythmguardrc.json` token sources and installed token
|
|
97
|
+
* packages, filtered to the property's kind. One same-unit token is a fix; a
|
|
98
|
+
* token in another unit, or several tokens, are named but never written.
|
|
99
|
+
*/
|
|
100
|
+
function resolveFromIndex(tokenIndex, parsedLength, prop, options) {
|
|
101
|
+
const px = toPx(Math.abs(parsedLength.number), parsedLength.unit || 'px', options.baseFontSize);
|
|
102
|
+
if (px === null || tokenIndex.length === 0) {
|
|
103
|
+
return { candidates: [] };
|
|
104
|
+
}
|
|
105
|
+
const unit = parsedLength.unit || 'px';
|
|
106
|
+
const sameUnit = tokensForLength(tokenIndex, px, unit, { prop });
|
|
107
|
+
const candidates = sameUnit.length > 0 || options.unitStrategy !== 'convert'
|
|
108
|
+
? sameUnit
|
|
109
|
+
: tokensForLength(tokenIndex, px, unit, { anyUnit: true, prop });
|
|
110
|
+
if (candidates.length !== 1) {
|
|
111
|
+
return { candidates };
|
|
112
|
+
}
|
|
113
|
+
const token = `var(${candidates[0]})`;
|
|
114
|
+
const entry = tokenIndex.find((item) => item.token === candidates[0]);
|
|
115
|
+
return {
|
|
116
|
+
candidates,
|
|
117
|
+
fixable: sameUnit.length === 1,
|
|
118
|
+
origin: entry ? entry.file : null,
|
|
119
|
+
replacement: applyNegativeToken(token, parsedLength),
|
|
120
|
+
token,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
77
124
|
const ruleFunction = (primary, secondaryOptions) => {
|
|
78
125
|
return (root, result) => {
|
|
79
126
|
const valid = validatePrimary(result, ruleName, primary);
|
|
@@ -105,6 +152,17 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
105
152
|
tokenRegex,
|
|
106
153
|
});
|
|
107
154
|
|
|
155
|
+
const tokenIndex = tokenIndexFromDefinitions(
|
|
156
|
+
collectTokenDefinitions({
|
|
157
|
+
baseFontSize: options.baseFontSize,
|
|
158
|
+
root,
|
|
159
|
+
scaleSources: options.scaleSources,
|
|
160
|
+
tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : null,
|
|
161
|
+
tokenRegex,
|
|
162
|
+
}),
|
|
163
|
+
options.baseFontSize,
|
|
164
|
+
);
|
|
165
|
+
|
|
108
166
|
const getScaleStateForProperty = createPropertyScaleResolver(options);
|
|
109
167
|
|
|
110
168
|
root.walkDecls((decl) => {
|
|
@@ -121,18 +179,31 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
121
179
|
const { scaleByUnit, scalePx } = getScaleStateForProperty(prop);
|
|
122
180
|
let changed = false;
|
|
123
181
|
|
|
124
|
-
const reportNode = (node,
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
182
|
+
const reportNode = (node, parsedLength, position = null) => {
|
|
183
|
+
const mapped = resolveTokenReplacement(tokenMap, node.value, parsedLength, options);
|
|
184
|
+
if (mapped && tokenFitsProperty(mapped.token, prop)) {
|
|
185
|
+
const origin = origins[mapped.token] || null;
|
|
186
|
+
reportValueNode({ decl, message: messages.rejected(node.value, mapped.replacement, origin, options.note, [], position), node, replacement: mapped.replacement, result, ruleName });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const indexed = resolveFromIndex(tokenIndex, parsedLength, prop, options);
|
|
190
|
+
const named = indexed.replacement || null;
|
|
191
|
+
reportValueNode({
|
|
192
|
+
decl,
|
|
193
|
+
message: messages.rejected(node.value, named, indexed.origin || null, options.note, indexed.candidates, position),
|
|
194
|
+
node,
|
|
195
|
+
replacement: indexed.fixable ? named : null,
|
|
196
|
+
result,
|
|
197
|
+
ruleName,
|
|
198
|
+
});
|
|
128
199
|
};
|
|
129
200
|
|
|
130
|
-
const checkWordNode = (node, context) => {
|
|
201
|
+
const checkWordNode = (node, context, position = null) => {
|
|
131
202
|
if (isKeyword(node.value, options.ignoreValues)) {
|
|
132
203
|
return false;
|
|
133
204
|
}
|
|
134
205
|
|
|
135
|
-
if (!shouldLintMathArgument(context, options)) {
|
|
206
|
+
if (!position && !shouldLintMathArgument(context, options)) {
|
|
136
207
|
return false;
|
|
137
208
|
}
|
|
138
209
|
|
|
@@ -190,7 +261,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
190
261
|
}
|
|
191
262
|
}
|
|
192
263
|
|
|
193
|
-
reportNode(node,
|
|
264
|
+
reportNode(node, parsedLength, position);
|
|
194
265
|
return true;
|
|
195
266
|
};
|
|
196
267
|
|
|
@@ -216,12 +287,22 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
216
287
|
return false;
|
|
217
288
|
});
|
|
218
289
|
} else {
|
|
290
|
+
const checkedBounds = new Set();
|
|
219
291
|
walkRootValueNodes(parsed, (node, context) => {
|
|
220
292
|
if (node.type === 'function') {
|
|
221
293
|
if (isTokenFunction(node, options.tokenFunctions, tokenRegex)) {
|
|
222
294
|
return true;
|
|
223
295
|
}
|
|
224
296
|
|
|
297
|
+
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
298
|
+
for (const bound of collectMathBounds(node)) {
|
|
299
|
+
if (!checkedBounds.has(bound.node)) {
|
|
300
|
+
checkedBounds.add(bound.node);
|
|
301
|
+
changed = checkWordNode(bound.node, null, bound.position) || changed;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
225
306
|
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
226
307
|
return true;
|
|
227
308
|
}
|
|
@@ -229,7 +310,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
229
310
|
return false;
|
|
230
311
|
}
|
|
231
312
|
|
|
232
|
-
if (node.type !== 'word') {
|
|
313
|
+
if (node.type !== 'word' || checkedBounds.has(node)) {
|
|
233
314
|
return false;
|
|
234
315
|
}
|
|
235
316
|
|
|
@@ -15,6 +15,8 @@ const {
|
|
|
15
15
|
createPropertyScaleResolver,
|
|
16
16
|
} = require('../../core/options');
|
|
17
17
|
const {
|
|
18
|
+
collectMathBounds,
|
|
19
|
+
isBoundFunction,
|
|
18
20
|
isKeyword,
|
|
19
21
|
isMathFunction,
|
|
20
22
|
isTokenFunction,
|
|
@@ -41,10 +43,20 @@ const ruleName = 'rhythmguard/use-scale';
|
|
|
41
43
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
42
44
|
invalidPreset: (presetName, presetNames) =>
|
|
43
45
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
44
|
-
rejected: (value, lower, upper, note = '') =>
|
|
45
|
-
`Unexpected off-scale value "${value}". Use scale values (nearest: ${lower} or ${upper}).${note ? ` ${note}` : ''}`,
|
|
46
|
+
rejected: (value, lower, upper, note = '', position = null, aside = '', group = null) =>
|
|
47
|
+
`Unexpected off-scale ${position ? `${position} "${value}"${aside}` : `value "${value}"`}. Use ${group ? `${group} ` : ''}scale values (nearest: ${lower} or ${upper}).${note ? ` ${note}` : ''}`,
|
|
46
48
|
});
|
|
47
49
|
|
|
50
|
+
/** " (16.8px)" after a bound written in rem or em, so the reader sees what was compared. */
|
|
51
|
+
function pxAside(value, baseFontSize) {
|
|
52
|
+
const parsed = parseLengthToken(value);
|
|
53
|
+
if (!parsed || !parsed.unit || parsed.unit === 'px' || parsed.unit === '%') {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
const px = toPx(Math.abs(parsed.number), parsed.unit, baseFontSize);
|
|
57
|
+
return px === null ? '' : ` (${formatLength(parsed.number < 0 ? -px : px, 'px')})`;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/** Decisions from .rhythmguardrc.json; an invalid section is reported once and ignored. */
|
|
49
61
|
function readDecisions(options, { result, root }) {
|
|
50
62
|
if (!options.readDecisions) {
|
|
@@ -136,7 +148,7 @@ function checkLengthValue({
|
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
const fixedValue = options.fixToScale && fixable
|
|
139
|
-
? replacementFor(parsedLength, nearest.nearest, options)
|
|
151
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
140
152
|
: null;
|
|
141
153
|
|
|
142
154
|
report(node.value, decl, node, nearest, fixedValue, unit, evaluated ? evaluatedNote(evaluated, options) : '');
|
|
@@ -160,7 +172,7 @@ function checkLengthValue({
|
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
const fixedValue = options.fixToScale && fixable
|
|
163
|
-
? replacementFor(parsedLength, nearest.nearest, options)
|
|
175
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
164
176
|
: null;
|
|
165
177
|
|
|
166
178
|
report(node.value, decl, node, nearest, fixedValue, 'px', evaluated ? evaluatedNote(evaluated, options) : '');
|
|
@@ -239,7 +251,13 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
239
251
|
const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
|
|
240
252
|
if (options.fixWith === 'token') {
|
|
241
253
|
options.tokenIndex = tokenIndexFromDefinitions(
|
|
242
|
-
collectTokenDefinitions({
|
|
254
|
+
collectTokenDefinitions({
|
|
255
|
+
baseFontSize: options.baseFontSize,
|
|
256
|
+
root,
|
|
257
|
+
scaleSources: options.scaleSources,
|
|
258
|
+
tokenPattern: options.tokenPatternExplicit ? options.tokenPattern : null,
|
|
259
|
+
tokenRegex,
|
|
260
|
+
}),
|
|
243
261
|
options.baseFontSize,
|
|
244
262
|
);
|
|
245
263
|
}
|
|
@@ -247,13 +265,13 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
247
265
|
let fallbackNote = autoScaleFallbackNote(options.scaleInference);
|
|
248
266
|
const getScaleStateForProperty = createPropertyScaleResolver(options);
|
|
249
267
|
|
|
250
|
-
const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px', extraNote = '') => {
|
|
268
|
+
const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px', extraNote = '', position = null) => {
|
|
251
269
|
const lower = nearest ? formatLength(nearest.lower, nearestUnit) : 'n/a';
|
|
252
270
|
const upper = nearest ? formatLength(nearest.upper, nearestUnit) : 'n/a';
|
|
253
271
|
const tokenNote = nearest ? tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit)) : '';
|
|
254
272
|
reportValueNode({
|
|
255
273
|
decl,
|
|
256
|
-
message: messages.rejected(value, lower, upper, [fallbackNote, tokenNote, extraNote, options.note].filter(Boolean).join(' ')),
|
|
274
|
+
message: messages.rejected(value, lower, upper, [fallbackNote, tokenNote, extraNote, options.note].filter(Boolean).join(' '), position, position ? pxAside(value, options.baseFontSize) : '', getScaleStateForProperty(decl.prop.toLowerCase()).group),
|
|
257
275
|
node,
|
|
258
276
|
replacement: fixedValue,
|
|
259
277
|
result,
|
|
@@ -336,6 +354,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
336
354
|
return false;
|
|
337
355
|
});
|
|
338
356
|
} else {
|
|
357
|
+
const checkedBounds = new Set();
|
|
339
358
|
walkRootValueNodes(parsed, (node, context) => {
|
|
340
359
|
|
|
341
360
|
if (node.type === 'function') {
|
|
@@ -343,6 +362,20 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
343
362
|
return true;
|
|
344
363
|
}
|
|
345
364
|
|
|
365
|
+
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
366
|
+
for (const bound of collectMathBounds(node)) {
|
|
367
|
+
if (checkedBounds.has(bound.node)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
checkedBounds.add(bound.node);
|
|
371
|
+
if (isKeyword(bound.node.value, options.ignoreValues)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const boundReport = (value, d, n, nearest, fixed, unit, extra) => report(value, d, n, nearest, fixed, unit, extra, bound.position);
|
|
375
|
+
changed = checkLengthValue({ decl, node: bound.node, options, report: boundReport, scaleByUnit, scalePx }) || changed;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
346
379
|
if (
|
|
347
380
|
isMathFunction(node.value) &&
|
|
348
381
|
!options.enforceInsideMathFunctions
|
|
@@ -353,7 +386,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
353
386
|
return false;
|
|
354
387
|
}
|
|
355
388
|
|
|
356
|
-
if (node.type !== 'word') {
|
|
389
|
+
if (node.type !== 'word' || checkedBounds.has(node)) {
|
|
357
390
|
return false;
|
|
358
391
|
}
|
|
359
392
|
|
package/types/audit.d.ts
CHANGED
|
@@ -32,7 +32,11 @@ export interface AuditOptions {
|
|
|
32
32
|
format?: "json" | "json-v1" | "markdown" | "text" | "html" | "github" | "badge";
|
|
33
33
|
ignorePath?: string;
|
|
34
34
|
ignorePatterns?: string[];
|
|
35
|
+
/** Check clamp() bounds and min()/max() arguments against the scale (equivalent to `--check-math-bounds`). */
|
|
36
|
+
checkMathBounds?: boolean;
|
|
35
37
|
includeMotion?: boolean;
|
|
38
|
+
/** A scale per property group (`spacing`, `radius`, `size`, `typography`): values, a preset name or "auto". Each group named is also checked. */
|
|
39
|
+
scales?: Partial<Record<"spacing" | "radius" | "size" | "typography", Array<number | string> | string | { preset?: string; scale?: Array<number | string>; customScale?: Array<number | string> }>>;
|
|
36
40
|
maxFindings?: number;
|
|
37
41
|
minCleanliness?: number;
|
|
38
42
|
/** Skip config discovery entirely (equivalent to `--no-config`). */
|
|
@@ -68,6 +72,8 @@ export interface AuditScanned {
|
|
|
68
72
|
scssFiles?: number;
|
|
69
73
|
/** .scss files found but not audited because postcss-scss is not installed. */
|
|
70
74
|
scssSkipped?: number;
|
|
75
|
+
/** Stylesheets Stylelint skipped because the project's own .stylelintignore excludes them. */
|
|
76
|
+
stylelintIgnored?: number;
|
|
71
77
|
templateFiles: number;
|
|
72
78
|
totalFiles?: number;
|
|
73
79
|
[key: string]: unknown;
|
package/types/shared.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ScaleSource {
|
|
|
17
17
|
export interface RhythmguardRuleOptions {
|
|
18
18
|
/** Exempt non-zero lengths of one CSS pixel or less (1px, -1px, 0.5px, 0.0625rem). Default true. */
|
|
19
19
|
allowHairlines?: boolean;
|
|
20
|
+
/** Check the static bounds of fluid values: the clamp() minimum and maximum and each length argument of min() and max(). Default false; the preferred value of clamp() is never checked. */
|
|
21
|
+
checkMathBounds?: boolean;
|
|
20
22
|
/** What autofix writes: the nearest literal (default) or, with "token", `var(--name)` when exactly one custom property holds that value in the same unit. */
|
|
21
23
|
fixWith?: "value" | "token";
|
|
22
24
|
/** Read the `decisions` section of `.rhythmguardrc.json` (default true). The audit sets false and applies decisions itself. */
|
|
@@ -34,6 +36,12 @@ export interface RhythmguardRuleOptions {
|
|
|
34
36
|
* properties, then `tailwindConfigPath`, falling back to the `rhythmic-4` preset.
|
|
35
37
|
*/
|
|
36
38
|
scale?: ScaleValue[] | "auto";
|
|
39
|
+
/**
|
|
40
|
+
* A scale per property group, used before `scale` and after `propertyScales`: values, a
|
|
41
|
+
* preset name, or `"auto"` to infer the group's scale from its tokens (`--font-size-*` and
|
|
42
|
+
* `--text-*` for typography, `--radius-*` for radius). Findings then name the group's scale.
|
|
43
|
+
*/
|
|
44
|
+
scales?: Partial<Record<"spacing" | "radius" | "size" | "typography", ScaleValue[] | string | { preset?: string; scale?: ScaleValue[]; customScale?: ScaleValue[] }>>;
|
|
37
45
|
/** Token files consulted first when `scale` is `"auto"`. */
|
|
38
46
|
scaleSources?: Array<string | ScaleSource>;
|
|
39
47
|
/** Tailwind v3 config whose `theme.spacing` feeds `scale: "auto"` and `tokenMapFromTailwindSpacing`. */
|