stylelint-plugin-rhythmguard 1.4.2 → 1.6.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 +8 -0
- package/README.md +150 -30
- package/package.json +10 -3
- package/src/cli/audit.js +645 -0
- package/src/cli/doctor.js +231 -0
- package/src/cli/index.js +39 -0
- package/src/cli/init.js +128 -0
- package/src/configs/react-tailwind.js +25 -0
- package/src/configs/react-tailwind.mjs +4 -0
- package/src/configs/tailwind.js +9 -0
- package/src/eslint/rules/tailwind-class-use-scale.js +25 -120
- package/src/utils/tailwind-class-analysis.js +221 -0
- package/src/utils/token-map.js +44 -3
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
formatLength,
|
|
5
|
+
nearestScaleValues,
|
|
6
|
+
normalizeScale,
|
|
7
|
+
numbersEqual,
|
|
8
|
+
parseLengthToken,
|
|
9
|
+
toPx,
|
|
10
|
+
} = require('./length');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SCALE = [0, 4, 8, 12, 16, 24, 32];
|
|
13
|
+
const DEFAULT_UNITS = ['px', 'rem', 'em'];
|
|
14
|
+
|
|
15
|
+
const ARBITRARY_SPACING_CLASS = /^(?<utility>-?(?:m(?:[trblxy])?|p(?:[trblxy])?|gap(?:-[xy])?|space-[xy]|inset(?:-[xy])?|top|right|bottom|left|translate-[xy]|scroll-(?:m|p)(?:[trblxy])?))-\[(?<rawValue>[^\]]+)\]$/;
|
|
16
|
+
|
|
17
|
+
function normalizeTailwindClassOptions(option = {}) {
|
|
18
|
+
return {
|
|
19
|
+
allowNegative: option.allowNegative !== false,
|
|
20
|
+
baseFontSize:
|
|
21
|
+
typeof option.baseFontSize === 'number' &&
|
|
22
|
+
Number.isFinite(option.baseFontSize) &&
|
|
23
|
+
option.baseFontSize > 0
|
|
24
|
+
? option.baseFontSize
|
|
25
|
+
: 16,
|
|
26
|
+
scale: Array.isArray(option.scale) ? option.scale : DEFAULT_SCALE,
|
|
27
|
+
units: Array.isArray(option.units)
|
|
28
|
+
? option.units.map((unit) => String(unit).toLowerCase())
|
|
29
|
+
: DEFAULT_UNITS,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findClassSegments(value) {
|
|
34
|
+
const segments = [];
|
|
35
|
+
const tokenRegex = /\S+/g;
|
|
36
|
+
let match;
|
|
37
|
+
|
|
38
|
+
while ((match = tokenRegex.exec(value)) !== null) {
|
|
39
|
+
segments.push({
|
|
40
|
+
start: match.index,
|
|
41
|
+
token: match[0],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return segments;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findLastVariantSeparator(token) {
|
|
49
|
+
let bracketDepth = 0;
|
|
50
|
+
let separatorIndex = -1;
|
|
51
|
+
|
|
52
|
+
for (let index = 0; index < token.length; index++) {
|
|
53
|
+
const character = token[index];
|
|
54
|
+
|
|
55
|
+
if (character === '[') {
|
|
56
|
+
bracketDepth++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (character === ']' && bracketDepth > 0) {
|
|
61
|
+
bracketDepth--;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (character === ':' && bracketDepth === 0) {
|
|
66
|
+
separatorIndex = index;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return separatorIndex;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseClassToken(token) {
|
|
74
|
+
const separatorIndex = findLastVariantSeparator(token);
|
|
75
|
+
const prefix = separatorIndex === -1
|
|
76
|
+
? ''
|
|
77
|
+
: token.slice(0, separatorIndex + 1);
|
|
78
|
+
let candidate = separatorIndex === -1
|
|
79
|
+
? token
|
|
80
|
+
: token.slice(separatorIndex + 1);
|
|
81
|
+
let leadingImportant = '';
|
|
82
|
+
let trailingImportant = '';
|
|
83
|
+
|
|
84
|
+
if (candidate.startsWith('!')) {
|
|
85
|
+
leadingImportant = '!';
|
|
86
|
+
candidate = candidate.slice(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (candidate.endsWith('!')) {
|
|
90
|
+
trailingImportant = '!';
|
|
91
|
+
candidate = candidate.slice(0, -1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
candidate,
|
|
96
|
+
leadingImportant,
|
|
97
|
+
prefix,
|
|
98
|
+
trailingImportant,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasInvalidVariantPrefix(prefix) {
|
|
103
|
+
let bracketDepth = 0;
|
|
104
|
+
|
|
105
|
+
for (const character of prefix) {
|
|
106
|
+
if (character === '[') {
|
|
107
|
+
bracketDepth++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (character === ']' && bracketDepth > 0) {
|
|
112
|
+
bracketDepth--;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (bracketDepth === 0 && /["'=<]/.test(character)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function analyzeClassToken(token, options, scalePx) {
|
|
125
|
+
const parsedToken = parseClassToken(token);
|
|
126
|
+
if (parsedToken.prefix && hasInvalidVariantPrefix(parsedToken.prefix)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const match = parsedToken.candidate.match(ARBITRARY_SPACING_CLASS);
|
|
131
|
+
if (!match || !match.groups) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsedLength = parseLengthToken(match.groups.rawValue);
|
|
136
|
+
if (!parsedLength || parsedLength.number === 0 || parsedLength.unit === '') {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!options.allowNegative && parsedLength.number < 0) {
|
|
141
|
+
return {
|
|
142
|
+
fixedToken: null,
|
|
143
|
+
nearest: null,
|
|
144
|
+
parsedLength,
|
|
145
|
+
reason: 'negative',
|
|
146
|
+
rawValue: match.groups.rawValue,
|
|
147
|
+
utility: match.groups.utility,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!options.units.includes(parsedLength.unit)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const pxValue = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
156
|
+
if (pxValue === null) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isOnScale = scalePx.some((entry) => numbersEqual(entry, pxValue));
|
|
161
|
+
if (isOnScale) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const nearest = nearestScaleValues(pxValue, scalePx);
|
|
166
|
+
if (!nearest) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const signedNearest = parsedLength.number < 0
|
|
171
|
+
? -Math.abs(nearest.nearest)
|
|
172
|
+
: nearest.nearest;
|
|
173
|
+
|
|
174
|
+
const replacementNumber = parsedLength.unit === 'px'
|
|
175
|
+
? signedNearest
|
|
176
|
+
: signedNearest / options.baseFontSize;
|
|
177
|
+
|
|
178
|
+
const replacementValue = formatLength(replacementNumber, parsedLength.unit);
|
|
179
|
+
const fixedCandidate = parsedToken.candidate.replace(match.groups.rawValue, replacementValue);
|
|
180
|
+
const fixedToken = `${parsedToken.prefix}${parsedToken.leadingImportant}${fixedCandidate}${parsedToken.trailingImportant}`;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
fixedToken,
|
|
184
|
+
nearest,
|
|
185
|
+
parsedLength,
|
|
186
|
+
reason: 'off-scale',
|
|
187
|
+
rawValue: match.groups.rawValue,
|
|
188
|
+
utility: match.groups.utility,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createTailwindClassAnalyzer(option = {}) {
|
|
193
|
+
const options = normalizeTailwindClassOptions(option);
|
|
194
|
+
const scalePx = normalizeScale(options.scale, options.baseFontSize);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
analyzeClassString(value) {
|
|
198
|
+
const findings = [];
|
|
199
|
+
|
|
200
|
+
for (const segment of findClassSegments(value)) {
|
|
201
|
+
const analysis = analyzeClassToken(segment.token, options, scalePx);
|
|
202
|
+
if (analysis) {
|
|
203
|
+
findings.push({ analysis, segment });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return findings;
|
|
208
|
+
},
|
|
209
|
+
analyzeToken(token) {
|
|
210
|
+
return analyzeClassToken(token, options, scalePx);
|
|
211
|
+
},
|
|
212
|
+
options,
|
|
213
|
+
scalePx,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
createTailwindClassAnalyzer,
|
|
219
|
+
findClassSegments,
|
|
220
|
+
normalizeTailwindClassOptions,
|
|
221
|
+
};
|
package/src/utils/token-map.js
CHANGED
|
@@ -79,6 +79,31 @@ function mergeExplicitTokenMap(target, source) {
|
|
|
79
79
|
return target;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function walkTokenGroup(map, group, prefix, baseFontSize) {
|
|
83
|
+
for (const [key, value] of Object.entries(group)) {
|
|
84
|
+
const tokenName = `${prefix}-${key}`;
|
|
85
|
+
|
|
86
|
+
if (!isPlainObject(value)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Leaf node with $value (DTCG)
|
|
91
|
+
if (typeof value.$value === 'string') {
|
|
92
|
+
addLengthValueMapping(map, value.$value, tokenName, baseFontSize);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Leaf node with value (Style Dictionary)
|
|
97
|
+
if (typeof value.value === 'string') {
|
|
98
|
+
addLengthValueMapping(map, value.value, tokenName, baseFontSize);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Nested group — recurse deeper
|
|
103
|
+
walkTokenGroup(map, value, tokenName, baseFontSize);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
function mergeTokenMapFromFile({
|
|
83
108
|
baseFontSize,
|
|
84
109
|
currentMap,
|
|
@@ -130,8 +155,21 @@ function mergeTokenMapFromFile({
|
|
|
130
155
|
continue;
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
if (isPlainObject(entryValue)
|
|
134
|
-
|
|
158
|
+
if (isPlainObject(entryValue)) {
|
|
159
|
+
// Style Dictionary format: { value: "16px" }
|
|
160
|
+
if (typeof entryValue.value === 'string') {
|
|
161
|
+
addLengthValueMapping(nextMap, entryValue.value, entryKey, baseFontSize);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// W3C DTCG format: { $value: "16px", $type: "dimension" }
|
|
166
|
+
if (typeof entryValue.$value === 'string') {
|
|
167
|
+
addLengthValueMapping(nextMap, entryValue.$value, entryKey, baseFontSize);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Nested group — recurse (e.g. { spacing: { 4: { $value: "16px" } } })
|
|
172
|
+
walkTokenGroup(nextMap, entryValue, entryKey, baseFontSize);
|
|
135
173
|
}
|
|
136
174
|
}
|
|
137
175
|
|
|
@@ -324,7 +362,7 @@ function buildEffectiveTokenMap({
|
|
|
324
362
|
root,
|
|
325
363
|
tokenRegex,
|
|
326
364
|
}) {
|
|
327
|
-
let tokenMap =
|
|
365
|
+
let tokenMap = {};
|
|
328
366
|
|
|
329
367
|
if (options.tokenMapFile) {
|
|
330
368
|
tokenMap = mergeTokenMapFromFile({
|
|
@@ -350,6 +388,9 @@ function buildEffectiveTokenMap({
|
|
|
350
388
|
});
|
|
351
389
|
}
|
|
352
390
|
|
|
391
|
+
// Explicit tokenMap applied last so it takes precedence over auto-derived tokens
|
|
392
|
+
tokenMap = mergeExplicitTokenMap(tokenMap, options.tokenMap);
|
|
393
|
+
|
|
353
394
|
return tokenMap;
|
|
354
395
|
}
|
|
355
396
|
|