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.
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ nearestScaleValues,
5
+ numbersEqual,
6
+ } = require('./length');
7
+ const {
8
+ formatTime,
9
+ fromMs,
10
+ normalizeDurationScale,
11
+ normalizeDurationUnits,
12
+ parseTimeToken,
13
+ toMs,
14
+ } = require('./time');
15
+ const { findClassSegments } = require('./tailwind-class-analysis');
16
+
17
+ const ARBITRARY_MOTION_CLASS = /^(?<utility>duration|delay|ease)-\[(?<rawValue>[^\]]+)\]$/;
18
+
19
+ function normalizeTailwindMotionOptions(option = {}) {
20
+ return {
21
+ durationScale: normalizeDurationScale(option.durationScale),
22
+ durationUnits: normalizeDurationUnits(option.durationUnits),
23
+ };
24
+ }
25
+
26
+ function findLastVariantSeparator(token) {
27
+ let bracketDepth = 0;
28
+ let separatorIndex = -1;
29
+
30
+ for (let index = 0; index < token.length; index++) {
31
+ const character = token[index];
32
+
33
+ if (character === '[') {
34
+ bracketDepth++;
35
+ continue;
36
+ }
37
+
38
+ if (character === ']' && bracketDepth > 0) {
39
+ bracketDepth--;
40
+ continue;
41
+ }
42
+
43
+ if (character === ':' && bracketDepth === 0) {
44
+ separatorIndex = index;
45
+ }
46
+ }
47
+
48
+ return separatorIndex;
49
+ }
50
+
51
+ function parseClassToken(token) {
52
+ const separatorIndex = findLastVariantSeparator(token);
53
+ const prefix = separatorIndex === -1
54
+ ? ''
55
+ : token.slice(0, separatorIndex + 1);
56
+ let candidate = separatorIndex === -1
57
+ ? token
58
+ : token.slice(separatorIndex + 1);
59
+ let leadingImportant = '';
60
+ let trailingImportant = '';
61
+
62
+ if (candidate.startsWith('!')) {
63
+ leadingImportant = '!';
64
+ candidate = candidate.slice(1);
65
+ }
66
+
67
+ if (candidate.endsWith('!')) {
68
+ trailingImportant = '!';
69
+ candidate = candidate.slice(0, -1);
70
+ }
71
+
72
+ return {
73
+ candidate,
74
+ leadingImportant,
75
+ prefix,
76
+ trailingImportant,
77
+ };
78
+ }
79
+
80
+ function analyzeMotionToken(token, options) {
81
+ const parsedToken = parseClassToken(token);
82
+ const match = parsedToken.candidate.match(ARBITRARY_MOTION_CLASS);
83
+ if (!match || !match.groups) {
84
+ return null;
85
+ }
86
+
87
+ if (match.groups.utility === 'ease') {
88
+ return {
89
+ fixedToken: null,
90
+ rawValue: match.groups.rawValue,
91
+ reason: 'easing',
92
+ utility: match.groups.utility,
93
+ };
94
+ }
95
+
96
+ const parsedTime = parseTimeToken(match.groups.rawValue);
97
+ if (!parsedTime) {
98
+ return null;
99
+ }
100
+
101
+ if (parsedTime.number < 0) {
102
+ return {
103
+ fixedToken: null,
104
+ nearest: null,
105
+ rawValue: match.groups.rawValue,
106
+ reason: 'negative',
107
+ utility: match.groups.utility,
108
+ };
109
+ }
110
+
111
+ if (!options.durationUnits.includes(parsedTime.unit)) {
112
+ return null;
113
+ }
114
+
115
+ const msValue = toMs(parsedTime.number, parsedTime.unit);
116
+ if (msValue === null) {
117
+ return null;
118
+ }
119
+
120
+ const isOnScale = options.durationScale.some((entry) => numbersEqual(entry, msValue));
121
+ if (isOnScale) {
122
+ return null;
123
+ }
124
+
125
+ const nearest = nearestScaleValues(msValue, options.durationScale);
126
+ if (!nearest) {
127
+ return null;
128
+ }
129
+
130
+ const converted = fromMs(nearest.nearest, parsedTime.unit);
131
+ if (converted === null) {
132
+ return null;
133
+ }
134
+
135
+ const replacementValue = formatTime(converted, parsedTime.unit);
136
+ const fixedCandidate = parsedToken.candidate.replace(match.groups.rawValue, replacementValue);
137
+ const fixedToken = `${parsedToken.prefix}${parsedToken.leadingImportant}${fixedCandidate}${parsedToken.trailingImportant}`;
138
+
139
+ return {
140
+ fixedToken,
141
+ nearest,
142
+ rawValue: match.groups.rawValue,
143
+ reason: 'duration',
144
+ utility: match.groups.utility,
145
+ };
146
+ }
147
+
148
+ function createTailwindMotionAnalyzer(option = {}) {
149
+ const options = normalizeTailwindMotionOptions(option);
150
+
151
+ return {
152
+ analyzeClassString(value) {
153
+ const findings = [];
154
+
155
+ for (const segment of findClassSegments(value)) {
156
+ const analysis = analyzeMotionToken(segment.token, options);
157
+ if (analysis) {
158
+ findings.push({ analysis, segment });
159
+ }
160
+ }
161
+
162
+ return findings;
163
+ },
164
+ analyzeToken(token) {
165
+ return analyzeMotionToken(token, options);
166
+ },
167
+ options,
168
+ };
169
+ }
170
+
171
+ module.exports = {
172
+ createTailwindMotionAnalyzer,
173
+ normalizeTailwindMotionOptions,
174
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const TIME_RE = /^(-?(?:\d+|\d*\.\d+))(ms|s)$/i;
4
+
5
+ function parseTimeToken(value) {
6
+ if (typeof value !== 'string') {
7
+ return null;
8
+ }
9
+
10
+ const match = value.trim().match(TIME_RE);
11
+ if (!match) {
12
+ return null;
13
+ }
14
+
15
+ const number = Number(match[1]);
16
+ if (!Number.isFinite(number)) {
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ number,
22
+ unit: match[2].toLowerCase(),
23
+ };
24
+ }
25
+
26
+ function toMs(number, unit) {
27
+ if (unit === 'ms') {
28
+ return number;
29
+ }
30
+
31
+ if (unit === 's') {
32
+ return number * 1000;
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ function fromMs(ms, unit) {
39
+ if (unit === 'ms') {
40
+ return ms;
41
+ }
42
+
43
+ if (unit === 's') {
44
+ return ms / 1000;
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ function formatTime(number, unit) {
51
+ const normalized = Object.is(number, -0) ? 0 : number;
52
+ if (Number.isInteger(normalized)) {
53
+ return `${normalized}${unit}`;
54
+ }
55
+
56
+ return `${Number(normalized.toFixed(4)).toString()}${unit}`;
57
+ }
58
+
59
+ function normalizeDurationScale(scale) {
60
+ const source = Array.isArray(scale) && scale.length > 0
61
+ ? scale
62
+ : [0, 75, 100, 150, 200, 300, 500, 700, 1000];
63
+
64
+ return [...new Set(source
65
+ .map((entry) => Number(entry))
66
+ .filter((entry) => Number.isFinite(entry) && entry >= 0))]
67
+ .sort((a, b) => a - b);
68
+ }
69
+
70
+ function normalizeDurationUnits(units) {
71
+ const source = Array.isArray(units) && units.length > 0
72
+ ? units
73
+ : ['ms', 's'];
74
+
75
+ const normalized = source
76
+ .map((unit) => String(unit).trim().toLowerCase())
77
+ .filter((unit) => unit === 'ms' || unit === 's');
78
+
79
+ return normalized.length > 0 ? [...new Set(normalized)] : ['ms', 's'];
80
+ }
81
+
82
+ module.exports = {
83
+ formatTime,
84
+ fromMs,
85
+ normalizeDurationScale,
86
+ normalizeDurationUnits,
87
+ parseTimeToken,
88
+ toMs,
89
+ };
@@ -22,12 +22,14 @@ const VALID_TOKEN_KINDS = new Set([
22
22
  'radius',
23
23
  'typography',
24
24
  'size',
25
+ 'motion',
25
26
  'all',
26
27
  ]);
27
28
 
28
29
  const TOKEN_KIND_PATTERNS = Object.freeze({
29
30
  all: /^--/,
30
31
  radius: /^--radius-/,
32
+ motion: /^--(?:motion|duration|delay|ease|easing)-/,
31
33
  size: /^--(?:size|width|height|container)-/,
32
34
  spacing: /^--(?:space|spacing)-/,
33
35
  typography: /^--(?:font|font-size|line-height|leading|tracking|typography)-/,
@@ -53,7 +55,7 @@ function normalizeTokenSourceFormat(format) {
53
55
  function normalizeTokenKind(kind) {
54
56
  const normalized = String(kind || 'spacing').trim().toLowerCase();
55
57
  if (!VALID_TOKEN_KINDS.has(normalized)) {
56
- throw new Error(`Invalid token kind "${kind}". Expected spacing, radius, typography, size, or all.`);
58
+ throw new Error(`Invalid token kind "${kind}". Expected spacing, radius, typography, size, motion, or all.`);
57
59
  }
58
60
 
59
61
  return normalized;