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.
@@ -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
+ };
@@ -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) && typeof entryValue.value === 'string') {
134
- addLengthValueMapping(nextMap, entryValue.value, entryKey, baseFontSize);
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 = mergeExplicitTokenMap({}, options.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