stylelint-plugin-rhythmguard 1.8.0 → 2.0.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 +24 -0
- package/README.md +85 -2
- package/package.json +13 -1
- package/src/audit/index.js +17 -0
- package/src/audit/index.mjs +11 -0
- package/src/cli/audit.js +476 -115
- package/src/cli/doctor.js +95 -0
- package/src/cli/index.js +1 -1
- package/src/configs/motion.js +13 -0
- package/src/configs/motion.mjs +4 -0
- package/src/eslint/index.js +3 -0
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +116 -0
- package/src/index.js +5 -1
- package/src/index.mjs +1 -0
- package/src/rules/use-motion-scale/index.js +245 -0
- package/src/rules/use-motion-scale/index.mjs +7 -0
- package/src/utils/constants.js +4 -0
- package/src/utils/tailwind-motion-analysis.js +174 -0
- package/src/utils/time.js +89 -0
- package/src/utils/token-sources.js +3 -1
package/src/cli/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const { normalizeTokenSourceFormat } = require('../utils/token-sources');
|
|
5
6
|
|
|
6
7
|
const cwd = process.cwd();
|
|
7
8
|
let issues = 0;
|
|
@@ -205,6 +206,99 @@ function checkCustomSyntax(configContent) {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
function checkRhythmguardConfig() {
|
|
210
|
+
const configPath = path.join(cwd, '.rhythmguardrc.json');
|
|
211
|
+
if (!fs.existsSync(configPath)) {
|
|
212
|
+
skip('rhythmguard config check skipped (.rhythmguardrc.json not found)');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
219
|
+
} catch {
|
|
220
|
+
fail('.rhythmguardrc.json is not valid JSON', 'Fix or remove .rhythmguardrc.json');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const audit = parsed.audit;
|
|
225
|
+
if (!audit || typeof audit !== 'object' || Array.isArray(audit)) {
|
|
226
|
+
fail('.rhythmguardrc.json audit config missing', 'Add an "audit" object or remove the config file');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pass('.rhythmguardrc.json audit config valid');
|
|
231
|
+
checkRhythmguardTokenSources(audit, path.dirname(configPath));
|
|
232
|
+
checkRhythmguardMotionConfig(audit);
|
|
233
|
+
checkRhythmguardBaseline(audit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function checkRhythmguardTokenSources(audit, baseDir) {
|
|
237
|
+
if (audit.tokenSources === undefined) {
|
|
238
|
+
skip('token source config check skipped (not configured)');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!Array.isArray(audit.tokenSources)) {
|
|
243
|
+
fail('audit.tokenSources must be an array', 'Use strings or { "path": "...", "format": "..." } entries');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const source of audit.tokenSources) {
|
|
248
|
+
const sourcePath = typeof source === 'string' ? source : source && source.path;
|
|
249
|
+
const format = typeof source === 'string' ? 'auto' : source && source.format;
|
|
250
|
+
|
|
251
|
+
if (typeof sourcePath !== 'string' || sourcePath.trim().length === 0) {
|
|
252
|
+
fail('token source entry missing path', 'Use strings or objects with a non-empty path');
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
normalizeTokenSourceFormat(format || 'auto');
|
|
258
|
+
} catch {
|
|
259
|
+
fail(`token source format invalid for ${sourcePath}`, 'Use auto, css, flat-json, style-dictionary, or dtcg');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (fs.existsSync(path.resolve(baseDir, sourcePath))) {
|
|
263
|
+
pass(`token source found (${sourcePath})`);
|
|
264
|
+
} else {
|
|
265
|
+
fail(`token source not found (${sourcePath})`, 'Update audit.tokenSources or create the token file');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function checkRhythmguardMotionConfig(audit) {
|
|
271
|
+
if (audit.includeMotion === undefined) {
|
|
272
|
+
skip('motion audit check skipped (not configured)');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof audit.includeMotion === 'boolean') {
|
|
277
|
+
pass(`motion audit config valid (${audit.includeMotion})`);
|
|
278
|
+
} else {
|
|
279
|
+
fail('audit.includeMotion must be a boolean', 'Use true or false');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function checkRhythmguardBaseline(audit) {
|
|
284
|
+
const baselinePath = path.resolve(cwd, audit.baseline || '.rhythmguard-baseline.json');
|
|
285
|
+
if (!fs.existsSync(baselinePath)) {
|
|
286
|
+
skip('baseline freshness check skipped (baseline not found)');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
292
|
+
if (!Array.isArray(baseline.findings)) {
|
|
293
|
+
fail('baseline file does not include findings array', 'Regenerate it with rhythmguard audit --write-baseline');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
pass(`baseline file readable (${path.relative(cwd, baselinePath)})`);
|
|
297
|
+
} catch {
|
|
298
|
+
fail('baseline file is not valid JSON', 'Regenerate it with rhythmguard audit --write-baseline');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
208
302
|
function run() {
|
|
209
303
|
process.stdout.write('\nRhythmguard Doctor\n\n');
|
|
210
304
|
|
|
@@ -214,6 +308,7 @@ function run() {
|
|
|
214
308
|
checkTokenPattern(configContent);
|
|
215
309
|
checkTailwindConfig(configContent);
|
|
216
310
|
checkCustomSyntax(configContent);
|
|
311
|
+
checkRhythmguardConfig();
|
|
217
312
|
|
|
218
313
|
process.stdout.write('\n');
|
|
219
314
|
|
package/src/cli/index.js
CHANGED
package/src/eslint/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const tailwindClassUseScale = require('./rules/tailwind-class-use-scale');
|
|
4
|
+
const tailwindClassUseMotionScale = require('./rules/tailwind-class-use-motion-scale');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
rules: {
|
|
7
8
|
'tailwind-class-use-scale': tailwindClassUseScale,
|
|
9
|
+
'tailwind-class-use-motion-scale': tailwindClassUseMotionScale,
|
|
8
10
|
},
|
|
9
11
|
configs: {
|
|
10
12
|
recommended: {
|
|
11
13
|
rules: {
|
|
12
14
|
'rhythmguard-tailwind/tailwind-class-use-scale': 'warn',
|
|
15
|
+
'rhythmguard-tailwind/tailwind-class-use-motion-scale': 'off',
|
|
13
16
|
},
|
|
14
17
|
},
|
|
15
18
|
},
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatTime } = require('../../utils/time');
|
|
4
|
+
const { createTailwindMotionAnalyzer } = require('../../utils/tailwind-motion-analysis');
|
|
5
|
+
|
|
6
|
+
const RULE_NAME = 'tailwind-class-use-motion-scale';
|
|
7
|
+
|
|
8
|
+
function maybeCheckNodeText(node, sourceCode, context, analyzer, allowFix) {
|
|
9
|
+
const rawText = sourceCode.getText(node);
|
|
10
|
+
let value = null;
|
|
11
|
+
let quote = null;
|
|
12
|
+
|
|
13
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
14
|
+
value = node.value;
|
|
15
|
+
quote = rawText[0] === '"' || rawText[0] === "'" ? rawText[0] : '"';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (node.type === 'TemplateElement') {
|
|
19
|
+
value = node.value.raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!value || typeof value !== 'string') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const findings = analyzer.analyzeClassString(value);
|
|
27
|
+
if (findings.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let fixedValue = value;
|
|
32
|
+
let fixedValueOffset = 0;
|
|
33
|
+
|
|
34
|
+
for (const { analysis, segment } of findings) {
|
|
35
|
+
if (allowFix && analysis.reason === 'duration' && node.type === 'Literal') {
|
|
36
|
+
const replacementStart = segment.start + fixedValueOffset;
|
|
37
|
+
fixedValue = `${fixedValue.slice(0, replacementStart)}${analysis.fixedToken}${fixedValue.slice(replacementStart + segment.token.length)}`;
|
|
38
|
+
fixedValueOffset += analysis.fixedToken.length - segment.token.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fixedText = allowFix && node.type === 'Literal' && fixedValue !== value
|
|
43
|
+
? `${quote}${fixedValue.replace(new RegExp(quote, 'g'), `\\${quote}`)}${quote}`
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
for (const { analysis, segment } of findings) {
|
|
47
|
+
const lower = analysis.nearest
|
|
48
|
+
? formatTime(analysis.nearest.lower, 'ms')
|
|
49
|
+
: 'n/a';
|
|
50
|
+
const upper = analysis.nearest
|
|
51
|
+
? formatTime(analysis.nearest.upper, 'ms')
|
|
52
|
+
: 'n/a';
|
|
53
|
+
context.report({
|
|
54
|
+
message: buildMessage(analysis, segment, lower, upper),
|
|
55
|
+
node,
|
|
56
|
+
fix:
|
|
57
|
+
fixedText && analysis.reason === 'duration'
|
|
58
|
+
? (fixer) => fixer.replaceText(node, fixedText)
|
|
59
|
+
: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildMessage(analysis, segment, lower, upper) {
|
|
65
|
+
if (analysis.reason === 'easing') {
|
|
66
|
+
return `Unexpected Tailwind arbitrary motion easing "${segment.token}". Use motion tokens for easing decisions.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (analysis.reason === 'negative') {
|
|
70
|
+
return `Unexpected Tailwind arbitrary motion duration "${segment.token}". Use non-negative duration values.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `Unexpected Tailwind arbitrary motion duration "${segment.token}". Use duration scale values (nearest: ${lower} or ${upper}).`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
meta: {
|
|
78
|
+
docs: {
|
|
79
|
+
description: 'Enforce duration scale for Tailwind arbitrary motion utilities in class strings',
|
|
80
|
+
},
|
|
81
|
+
fixable: 'code',
|
|
82
|
+
schema: [
|
|
83
|
+
{
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
properties: {
|
|
86
|
+
durationScale: {
|
|
87
|
+
items: { type: 'number' },
|
|
88
|
+
type: 'array',
|
|
89
|
+
},
|
|
90
|
+
durationUnits: {
|
|
91
|
+
items: {
|
|
92
|
+
enum: ['ms', 's'],
|
|
93
|
+
type: 'string',
|
|
94
|
+
},
|
|
95
|
+
type: 'array',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
type: 'object',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
create(context) {
|
|
103
|
+
const analyzer = createTailwindMotionAnalyzer(context.options && context.options[0]);
|
|
104
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
Literal(node) {
|
|
108
|
+
maybeCheckNodeText(node, sourceCode, context, analyzer, true);
|
|
109
|
+
},
|
|
110
|
+
TemplateElement(node) {
|
|
111
|
+
maybeCheckNodeText(node, sourceCode, context, analyzer, false);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
ruleName: RULE_NAME,
|
|
116
|
+
};
|
package/src/index.js
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
const useScale = require('./rules/use-scale');
|
|
4
4
|
const preferToken = require('./rules/prefer-token');
|
|
5
5
|
const noOffscaleTransform = require('./rules/no-offscale-transform');
|
|
6
|
+
const useMotionScale = require('./rules/use-motion-scale');
|
|
6
7
|
|
|
7
|
-
const rules = [useScale, preferToken, noOffscaleTransform];
|
|
8
|
+
const rules = [useScale, preferToken, noOffscaleTransform, useMotionScale];
|
|
8
9
|
|
|
9
10
|
module.exports = rules;
|
|
10
11
|
module.exports.rules = {
|
|
11
12
|
[useScale.ruleName]: useScale,
|
|
12
13
|
[preferToken.ruleName]: preferToken,
|
|
13
14
|
[noOffscaleTransform.ruleName]: noOffscaleTransform,
|
|
15
|
+
[useMotionScale.ruleName]: useMotionScale,
|
|
14
16
|
};
|
|
15
17
|
module.exports.configs = {
|
|
16
18
|
recommended: require('./configs/recommended'),
|
|
@@ -19,6 +21,8 @@ module.exports.configs = {
|
|
|
19
21
|
expanded: require('./configs/expanded'),
|
|
20
22
|
logical: require('./configs/logical'),
|
|
21
23
|
migration: require('./configs/migration'),
|
|
24
|
+
motion: require('./configs/motion'),
|
|
22
25
|
};
|
|
23
26
|
module.exports.eslint = require('./eslint');
|
|
24
27
|
module.exports.presets = require('./presets');
|
|
28
|
+
module.exports.audit = require('./audit');
|
package/src/index.mjs
CHANGED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const stylelint = require('stylelint');
|
|
4
|
+
const valueParser = require('postcss-value-parser');
|
|
5
|
+
const {
|
|
6
|
+
nearestScaleValues,
|
|
7
|
+
numbersEqual,
|
|
8
|
+
} = require('../../utils/length');
|
|
9
|
+
const {
|
|
10
|
+
declarationValueIndex,
|
|
11
|
+
walkRootValueNodes,
|
|
12
|
+
} = require('../../utils/value-utils');
|
|
13
|
+
const {
|
|
14
|
+
formatTime,
|
|
15
|
+
fromMs,
|
|
16
|
+
normalizeDurationScale,
|
|
17
|
+
normalizeDurationUnits,
|
|
18
|
+
parseTimeToken,
|
|
19
|
+
toMs,
|
|
20
|
+
} = require('../../utils/time');
|
|
21
|
+
|
|
22
|
+
const ruleName = 'rhythmguard/use-motion-scale';
|
|
23
|
+
const DURATION_PROPERTIES = new Set([
|
|
24
|
+
'transition-duration',
|
|
25
|
+
'transition-delay',
|
|
26
|
+
'animation-duration',
|
|
27
|
+
'animation-delay',
|
|
28
|
+
'transition',
|
|
29
|
+
'animation',
|
|
30
|
+
]);
|
|
31
|
+
const EASING_PROPERTIES = new Set([
|
|
32
|
+
'transition-timing-function',
|
|
33
|
+
'animation-timing-function',
|
|
34
|
+
'transition',
|
|
35
|
+
'animation',
|
|
36
|
+
]);
|
|
37
|
+
const EASING_FUNCTIONS = new Set(['cubic-bezier', 'linear', 'steps']);
|
|
38
|
+
|
|
39
|
+
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
40
|
+
invalidDuration: (value) =>
|
|
41
|
+
`Unexpected negative motion duration "${value}". Use non-negative duration values.`,
|
|
42
|
+
rejectedDuration: (value, lower, upper) =>
|
|
43
|
+
`Unexpected motion duration "${value}". Use duration scale values (nearest: ${lower} or ${upper}).`,
|
|
44
|
+
rejectedEasing: (value) =>
|
|
45
|
+
`Unexpected raw motion easing "${value}". Use motion tokens for easing decisions.`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function isPlainObject(value) {
|
|
49
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildOptions(rawOptions) {
|
|
53
|
+
const options = rawOptions || {};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
durationScale: normalizeDurationScale(options.durationScale),
|
|
57
|
+
durationUnits: normalizeDurationUnits(options.durationUnits),
|
|
58
|
+
easingTokenMap: isPlainObject(options.easingTokenMap) ? options.easingTokenMap : {},
|
|
59
|
+
fixToScale: options.fixToScale !== false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateSecondaryOptions(result, secondaryOptions) {
|
|
64
|
+
return stylelint.utils.validateOptions(result, ruleName, {
|
|
65
|
+
actual: secondaryOptions,
|
|
66
|
+
optional: true,
|
|
67
|
+
possible: {
|
|
68
|
+
durationScale: [(value) =>
|
|
69
|
+
typeof value === 'number' &&
|
|
70
|
+
Number.isFinite(value) &&
|
|
71
|
+
value >= 0],
|
|
72
|
+
durationUnits: [(value) => value === 'ms' || value === 's'],
|
|
73
|
+
easingTokenMap: [(value) =>
|
|
74
|
+
isPlainObject(value) &&
|
|
75
|
+
Object.values(value).every((entry) =>
|
|
76
|
+
typeof entry === 'string' && entry.trim().length > 0,
|
|
77
|
+
)],
|
|
78
|
+
fixToScale: [true, false],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getFixedNodeValue(parsedTime, nearestMs, options) {
|
|
84
|
+
if (!options.durationUnits.includes(parsedTime.unit)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const converted = fromMs(nearestMs, parsedTime.unit);
|
|
89
|
+
if (converted === null) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return formatTime(converted, parsedTime.unit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function functionToString(node) {
|
|
97
|
+
return valueParser.stringify(node);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ruleFunction = (primary, secondaryOptions) => {
|
|
101
|
+
return (root, result) => {
|
|
102
|
+
const valid = stylelint.utils.validateOptions(result, ruleName, {
|
|
103
|
+
actual: primary,
|
|
104
|
+
possible: [true],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!valid || !validateSecondaryOptions(result, secondaryOptions)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const options = buildOptions(secondaryOptions);
|
|
112
|
+
|
|
113
|
+
root.walkDecls((decl) => {
|
|
114
|
+
const prop = decl.prop.toLowerCase();
|
|
115
|
+
if (!DURATION_PROPERTIES.has(prop) && !EASING_PROPERTIES.has(prop)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const parsed = valueParser(decl.value);
|
|
120
|
+
let changed = false;
|
|
121
|
+
|
|
122
|
+
const reportDuration = (node, nearest, fixedValue = null) => {
|
|
123
|
+
const index = declarationValueIndex(decl) + node.sourceIndex;
|
|
124
|
+
const payload = {
|
|
125
|
+
endIndex: index + node.value.length,
|
|
126
|
+
index,
|
|
127
|
+
message: nearest
|
|
128
|
+
? messages.rejectedDuration(
|
|
129
|
+
node.value,
|
|
130
|
+
formatTime(nearest.lower, 'ms'),
|
|
131
|
+
formatTime(nearest.upper, 'ms'),
|
|
132
|
+
)
|
|
133
|
+
: messages.invalidDuration(node.value),
|
|
134
|
+
node: decl,
|
|
135
|
+
result,
|
|
136
|
+
ruleName,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (fixedValue) {
|
|
140
|
+
payload.fix = () => {
|
|
141
|
+
node.value = fixedValue;
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stylelint.utils.report(payload);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const reportEasing = (node, replacement = null) => {
|
|
150
|
+
const source = functionToString(node);
|
|
151
|
+
const index = declarationValueIndex(decl) + node.sourceIndex;
|
|
152
|
+
const payload = {
|
|
153
|
+
endIndex: index + source.length,
|
|
154
|
+
index,
|
|
155
|
+
message: messages.rejectedEasing(source),
|
|
156
|
+
node: decl,
|
|
157
|
+
result,
|
|
158
|
+
ruleName,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (replacement) {
|
|
162
|
+
payload.fix = () => {
|
|
163
|
+
node.type = 'word';
|
|
164
|
+
node.value = replacement;
|
|
165
|
+
delete node.nodes;
|
|
166
|
+
return true;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
stylelint.utils.report(payload);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
walkRootValueNodes(parsed, (node) => {
|
|
174
|
+
if (node.type === 'function') {
|
|
175
|
+
const functionName = node.value.toLowerCase();
|
|
176
|
+
if (!EASING_PROPERTIES.has(prop) || !EASING_FUNCTIONS.has(functionName)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const source = functionToString(node);
|
|
181
|
+
reportEasing(node, options.easingTokenMap[source] || null);
|
|
182
|
+
changed = changed || Boolean(options.easingTokenMap[source]);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (node.type !== 'word' || !DURATION_PROPERTIES.has(prop)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parsedTime = parseTimeToken(node.value);
|
|
191
|
+
if (!parsedTime) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (parsedTime.number < 0) {
|
|
196
|
+
reportDuration(node, null);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!options.durationUnits.includes(parsedTime.unit)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const msValue = toMs(parsedTime.number, parsedTime.unit);
|
|
205
|
+
if (msValue === null) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isOnScale = options.durationScale.some((scaleValue) => numbersEqual(scaleValue, msValue));
|
|
210
|
+
if (isOnScale) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const nearest = nearestScaleValues(msValue, options.durationScale);
|
|
215
|
+
if (!nearest) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const fixedValue = options.fixToScale
|
|
220
|
+
? getFixedNodeValue(parsedTime, nearest.nearest, options)
|
|
221
|
+
: null;
|
|
222
|
+
|
|
223
|
+
reportDuration(node, nearest, fixedValue);
|
|
224
|
+
changed = changed || Boolean(fixedValue);
|
|
225
|
+
return false;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (changed) {
|
|
229
|
+
decl.value = parsed.toString();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
ruleFunction.ruleName = ruleName;
|
|
236
|
+
ruleFunction.messages = messages;
|
|
237
|
+
ruleFunction.meta = {
|
|
238
|
+
fixable: true,
|
|
239
|
+
url: 'https://github.com/petrilahdelma/stylelint-plugin-rhythmguard#rhythmguarduse-motion-scale',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
module.exports = stylelint.createPlugin(ruleName, ruleFunction);
|
|
243
|
+
module.exports.ruleName = ruleName;
|
|
244
|
+
module.exports.messages = messages;
|
|
245
|
+
module.exports.meta = ruleFunction.meta;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const rule = require('./index.js');
|
|
4
|
+
export default rule;
|
|
5
|
+
export const ruleName = rule.ruleName;
|
|
6
|
+
export const messages = rule.messages;
|
|
7
|
+
export const meta = rule.meta;
|
package/src/utils/constants.js
CHANGED
|
@@ -35,6 +35,10 @@ const PROPERTY_GROUP_PATTERNS = Object.freeze({
|
|
|
35
35
|
/^letter-spacing$/,
|
|
36
36
|
/^word-spacing$/,
|
|
37
37
|
]),
|
|
38
|
+
motion: Object.freeze([
|
|
39
|
+
/^transition(?:-.+)?$/,
|
|
40
|
+
/^animation(?:-.+)?$/,
|
|
41
|
+
]),
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
const DEFAULT_PROPERTY_GROUPS = Object.freeze(['spacing']);
|