scss-variable-extractor 1.6.4 → 1.6.6
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/.prettierignore +7 -0
- package/.prettierrc.json +18 -0
- package/.scssextractrc.example.json +31 -35
- package/MULTI-APP-GUIDE.md +262 -0
- package/README.md +1283 -778
- package/THEME-GUIDE.md +289 -0
- package/bin/cli.js +1002 -652
- package/jest.config.js +7 -12
- package/multi-app.example.json +44 -0
- package/package.json +50 -45
- package/src/analyzer.js +285 -285
- package/src/angular-parser.js +383 -381
- package/src/bootstrap-migrator.js +694 -661
- package/src/config.js +87 -91
- package/src/generator.js +220 -219
- package/src/index.js +37 -29
- package/src/multi-app-analyzer.js +612 -0
- package/src/ng-refactorer.js +654 -578
- package/src/parser.js +424 -421
- package/src/refactorer.js +329 -322
- package/src/scanner.js +63 -55
- package/src/style-organizer.js +500 -504
- package/src/theme-utils.js +432 -0
- package/test/analyzer.test.js +107 -107
- package/test/angular-parser.test.js +230 -230
- package/test/bootstrap-migrator.test.js +230 -213
- package/test/generator.test.js +139 -149
- package/test/multi-app-analyzer.test.js +216 -0
- package/test/ng-refactorer-global.test.js +140 -0
- package/test/ng-refactorer.test.js +191 -184
- package/test/parser.test.js +131 -131
- package/test/refactorer-edge-cases.test.js +385 -353
- package/test/refactorer.test.js +277 -257
- package/test/scanner.test.js +34 -32
- package/test/style-organizer.test.js +106 -106
- package/test/theme-utils.test.js +140 -0
package/src/refactorer.js
CHANGED
|
@@ -1,322 +1,329 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Refactors SCSS files by replacing hardcoded values with variable references
|
|
6
|
-
* @param {Array<string>} scssFiles - Array of SCSS file paths
|
|
7
|
-
* @param {Object} analysis - Analysis results with variable mappings
|
|
8
|
-
* @param {string} variablesFilePath - Path to the generated variables file
|
|
9
|
-
* @param {Object} config - Configuration
|
|
10
|
-
*/
|
|
11
|
-
function refactorScssFiles(scssFiles, analysis, variablesFilePath, config) {
|
|
12
|
-
const refactoredFiles = [];
|
|
13
|
-
|
|
14
|
-
scssFiles.forEach(filePath => {
|
|
15
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
-
const refactored = refactorFile(content, analysis, variablesFilePath, filePath, config);
|
|
17
|
-
|
|
18
|
-
if (refactored.modified) {
|
|
19
|
-
fs.writeFileSync(filePath, refactored.content, 'utf8');
|
|
20
|
-
refactoredFiles.push({
|
|
21
|
-
path: filePath,
|
|
22
|
-
changes: refactored.changes
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return refactoredFiles;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Refactors a single SCSS file
|
|
32
|
-
*/
|
|
33
|
-
function refactorFile(content, analysis, variablesFilePath, currentFilePath, config) {
|
|
34
|
-
let newContent = content;
|
|
35
|
-
let modified = false;
|
|
36
|
-
const changes = [];
|
|
37
|
-
|
|
38
|
-
// Build value-to-variable mapping
|
|
39
|
-
const valueMap = buildValueMap(analysis);
|
|
40
|
-
|
|
41
|
-
// Check if file already has @use import for variables
|
|
42
|
-
const hasVariablesImport = content.includes('@use') && content.includes('variables');
|
|
43
|
-
|
|
44
|
-
// Replace values with variables
|
|
45
|
-
// Sort by value length (longest first) to avoid partial replacements
|
|
46
|
-
// E.g., replace "0.5" before "0" to prevent "0.5" becoming "$var.5"
|
|
47
|
-
const sortedEntries = Object.entries(valueMap).sort((a, b) => b[0].length - a[0].length);
|
|
48
|
-
|
|
49
|
-
sortedEntries.forEach(([value, variableName]) => {
|
|
50
|
-
const regex = createReplacementRegex(value);
|
|
51
|
-
const matches = [];
|
|
52
|
-
|
|
53
|
-
let match;
|
|
54
|
-
// Find matches in the current state of newContent (after previous replacements)
|
|
55
|
-
while ((match = regex.exec(newContent)) !== null) {
|
|
56
|
-
matches.push({
|
|
57
|
-
index: match.index,
|
|
58
|
-
matched: match[0],
|
|
59
|
-
fullMatch: match
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check if replacements are in safe zones
|
|
64
|
-
// Process matches in reverse order (highest index first) to avoid position shifts
|
|
65
|
-
matches.reverse().forEach(m => {
|
|
66
|
-
const inSafeZone = isInSafeZone(newContent, m.index, m.matched);
|
|
67
|
-
|
|
68
|
-
if (!inSafeZone) {
|
|
69
|
-
// Replace the value
|
|
70
|
-
const before = newContent;
|
|
71
|
-
newContent = replaceAt(newContent, m.matched, variableName, m.index);
|
|
72
|
-
|
|
73
|
-
if (before !== newContent) {
|
|
74
|
-
modified = true;
|
|
75
|
-
changes.push({
|
|
76
|
-
from: value,
|
|
77
|
-
to: variableName
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Add @use import if modified and not already present
|
|
85
|
-
if (modified && !hasVariablesImport) {
|
|
86
|
-
const relativePath = getRelativeImportPath(currentFilePath, variablesFilePath);
|
|
87
|
-
const useStatement = `@use '${relativePath}' as *;\n\n`;
|
|
88
|
-
newContent = useStatement + newContent;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
content: newContent,
|
|
93
|
-
modified,
|
|
94
|
-
changes
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Builds a map of values to variable names
|
|
100
|
-
*/
|
|
101
|
-
function buildValueMap(analysis) {
|
|
102
|
-
const valueMap = {};
|
|
103
|
-
|
|
104
|
-
Object.keys(analysis).forEach(category => {
|
|
105
|
-
analysis[category].forEach(item => {
|
|
106
|
-
// Use normalized value as key for consistent matching
|
|
107
|
-
valueMap[item.value] = item.suggestedName;
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return valueMap;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Creates a regex for replacing a specific value
|
|
116
|
-
*/
|
|
117
|
-
function createReplacementRegex(value) {
|
|
118
|
-
// Escape special regex characters
|
|
119
|
-
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
120
|
-
|
|
121
|
-
// Add appropriate boundaries based on value type to prevent partial matches
|
|
122
|
-
|
|
123
|
-
if (/^#[0-9a-fA-F]+$/.test(value)) {
|
|
124
|
-
// Hex colors - no word boundary before #, but check after
|
|
125
|
-
return new RegExp(`${escaped}\\b`, 'g');
|
|
126
|
-
} else if (/\d+(?:px|%|em|rem|vh|vw|pt|ch|ex)$/.test(value)) {
|
|
127
|
-
// Values with CSS units - ensure not part of larger number
|
|
128
|
-
return new RegExp(`(?<!\\d)${escaped}\\b`, 'g');
|
|
129
|
-
} else if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
130
|
-
// Pure numbers/decimals (unitless) - ensure not part of larger number or before units
|
|
131
|
-
// Negative lookbehind: not preceded by digit
|
|
132
|
-
// Negative lookahead: not followed by digit, unit letters, or % (for keyframe selectors)
|
|
133
|
-
return new RegExp(`(?<!\\d)${escaped}(?!\\d|[a-z%])`, 'g');
|
|
134
|
-
} else if (/^rgba?\(/.test(value)) {
|
|
135
|
-
// RGB/RGBA colors - match complete function
|
|
136
|
-
return new RegExp(escaped, 'g');
|
|
137
|
-
} else {
|
|
138
|
-
// Other values (keywords, etc.) - use word boundaries
|
|
139
|
-
return new RegExp(`\\b${escaped}\\b`, 'g');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Checks if a position in content is in a safe zone (should not be replaced)
|
|
145
|
-
*/
|
|
146
|
-
function isInSafeZone(content, position, matchedValue) {
|
|
147
|
-
const DEBUG = false; // Enable for debugging
|
|
148
|
-
|
|
149
|
-
// Check for variable declarations
|
|
150
|
-
const lineStart = content.lastIndexOf('\n', position);
|
|
151
|
-
const lineEnd = content.indexOf('\n', position);
|
|
152
|
-
const line = content.substring(lineStart, lineEnd);
|
|
153
|
-
|
|
154
|
-
// Check if this line is a variable declaration: $variable-name: value;
|
|
155
|
-
// Match pattern: optional whitespace, $, identifier, optional whitespace, colon
|
|
156
|
-
const varDeclMatch = line.match(/^\s*\$([\w-]+)\s*:/);
|
|
157
|
-
if (varDeclMatch) {
|
|
158
|
-
// This line IS a variable declaration
|
|
159
|
-
if (DEBUG) console.log(` → SAFE: Variable declaration line`);
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check if we're in the middle of a larger number or value
|
|
164
|
-
const charBefore = content.charAt(position - 1);
|
|
165
|
-
const charAfter = content.charAt(position + matchedValue.length);
|
|
166
|
-
|
|
167
|
-
// Prevent replacement if surrounded by digits (partial number match)
|
|
168
|
-
if (/\d/.test(charBefore) || (/^\d+$/.test(matchedValue) && /\d/.test(charAfter))) {
|
|
169
|
-
if (DEBUG) console.log(` → SAFE: Surrounded by digits`);
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Prevent replacement within hex colors
|
|
174
|
-
if (/[0-9a-fA-F]/.test(charBefore) || /[0-9a-fA-F]/.test(charAfter)) {
|
|
175
|
-
// Look back further to check if we're in a hex color
|
|
176
|
-
const lookBehind = content.substring(Math.max(0, position - 10), position);
|
|
177
|
-
if (/#[0-9a-fA-F]*$/.test(lookBehind)) {
|
|
178
|
-
if (DEBUG) console.log(` → SAFE: Inside hex color`);
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Prevent replacement if it would create invalid variable concatenation
|
|
184
|
-
if (charBefore === '$' || charAfter === '$') {
|
|
185
|
-
if (DEBUG) console.log(` → SAFE: Near $ symbol`);
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Prevent replacement for negative values (e.g., -16px should not become -$spacing-md)
|
|
190
|
-
if (charBefore === '-' && /^\d/.test(matchedValue)) {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check for @use, @forward, @import
|
|
195
|
-
if (/@(?:use|forward|import)/.test(line)) {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Check for url(), calc(), transform functions, and other contexts
|
|
200
|
-
const beforeContext = content.substring(Math.max(0, position - 100), position);
|
|
201
|
-
|
|
202
|
-
// Prevent replacement inside calc() expressions
|
|
203
|
-
if (/calc\s*\([^)]*$/.test(beforeContext)) {
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Prevent replacement inside transform functions
|
|
208
|
-
if (/(?:translate|rotate|scale|skew|matrix|perspective)\s*\([^)]*$/.test(beforeContext)) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (/url\s*\(\s*[^)]*$/.test(beforeContext)) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check for content property
|
|
217
|
-
if (/content\s*:\s*[^;]*$/.test(beforeContext)) {
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Check for interpolation #{}
|
|
222
|
-
if (/#\{[^}]*$/.test(beforeContext)) {
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Check for Material functions
|
|
227
|
-
if (/mat\.\w+\([^)]*$/.test(beforeContext)) {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Prevent replacement inside attribute selectors
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Refactors SCSS files by replacing hardcoded values with variable references
|
|
6
|
+
* @param {Array<string>} scssFiles - Array of SCSS file paths
|
|
7
|
+
* @param {Object} analysis - Analysis results with variable mappings
|
|
8
|
+
* @param {string} variablesFilePath - Path to the generated variables file
|
|
9
|
+
* @param {Object} config - Configuration
|
|
10
|
+
*/
|
|
11
|
+
function refactorScssFiles(scssFiles, analysis, variablesFilePath, config) {
|
|
12
|
+
const refactoredFiles = [];
|
|
13
|
+
|
|
14
|
+
scssFiles.forEach(filePath => {
|
|
15
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
const refactored = refactorFile(content, analysis, variablesFilePath, filePath, config);
|
|
17
|
+
|
|
18
|
+
if (refactored.modified) {
|
|
19
|
+
fs.writeFileSync(filePath, refactored.content, 'utf8');
|
|
20
|
+
refactoredFiles.push({
|
|
21
|
+
path: filePath,
|
|
22
|
+
changes: refactored.changes,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return refactoredFiles;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Refactors a single SCSS file
|
|
32
|
+
*/
|
|
33
|
+
function refactorFile(content, analysis, variablesFilePath, currentFilePath, config) {
|
|
34
|
+
let newContent = content;
|
|
35
|
+
let modified = false;
|
|
36
|
+
const changes = [];
|
|
37
|
+
|
|
38
|
+
// Build value-to-variable mapping
|
|
39
|
+
const valueMap = buildValueMap(analysis);
|
|
40
|
+
|
|
41
|
+
// Check if file already has @use import for variables
|
|
42
|
+
const hasVariablesImport = content.includes('@use') && content.includes('variables');
|
|
43
|
+
|
|
44
|
+
// Replace values with variables
|
|
45
|
+
// Sort by value length (longest first) to avoid partial replacements
|
|
46
|
+
// E.g., replace "0.5" before "0" to prevent "0.5" becoming "$var.5"
|
|
47
|
+
const sortedEntries = Object.entries(valueMap).sort((a, b) => b[0].length - a[0].length);
|
|
48
|
+
|
|
49
|
+
sortedEntries.forEach(([value, variableName]) => {
|
|
50
|
+
const regex = createReplacementRegex(value);
|
|
51
|
+
const matches = [];
|
|
52
|
+
|
|
53
|
+
let match;
|
|
54
|
+
// Find matches in the current state of newContent (after previous replacements)
|
|
55
|
+
while ((match = regex.exec(newContent)) !== null) {
|
|
56
|
+
matches.push({
|
|
57
|
+
index: match.index,
|
|
58
|
+
matched: match[0],
|
|
59
|
+
fullMatch: match,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if replacements are in safe zones
|
|
64
|
+
// Process matches in reverse order (highest index first) to avoid position shifts
|
|
65
|
+
matches.reverse().forEach(m => {
|
|
66
|
+
const inSafeZone = isInSafeZone(newContent, m.index, m.matched);
|
|
67
|
+
|
|
68
|
+
if (!inSafeZone) {
|
|
69
|
+
// Replace the value
|
|
70
|
+
const before = newContent;
|
|
71
|
+
newContent = replaceAt(newContent, m.matched, variableName, m.index);
|
|
72
|
+
|
|
73
|
+
if (before !== newContent) {
|
|
74
|
+
modified = true;
|
|
75
|
+
changes.push({
|
|
76
|
+
from: value,
|
|
77
|
+
to: variableName,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Add @use import if modified and not already present
|
|
85
|
+
if (modified && !hasVariablesImport) {
|
|
86
|
+
const relativePath = getRelativeImportPath(currentFilePath, variablesFilePath);
|
|
87
|
+
const useStatement = `@use '${relativePath}' as *;\n\n`;
|
|
88
|
+
newContent = useStatement + newContent;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: newContent,
|
|
93
|
+
modified,
|
|
94
|
+
changes,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds a map of values to variable names
|
|
100
|
+
*/
|
|
101
|
+
function buildValueMap(analysis) {
|
|
102
|
+
const valueMap = {};
|
|
103
|
+
|
|
104
|
+
Object.keys(analysis).forEach(category => {
|
|
105
|
+
analysis[category].forEach(item => {
|
|
106
|
+
// Use normalized value as key for consistent matching
|
|
107
|
+
valueMap[item.value] = item.suggestedName;
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return valueMap;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a regex for replacing a specific value
|
|
116
|
+
*/
|
|
117
|
+
function createReplacementRegex(value) {
|
|
118
|
+
// Escape special regex characters
|
|
119
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
120
|
+
|
|
121
|
+
// Add appropriate boundaries based on value type to prevent partial matches
|
|
122
|
+
|
|
123
|
+
if (/^#[0-9a-fA-F]+$/.test(value)) {
|
|
124
|
+
// Hex colors - no word boundary before #, but check after
|
|
125
|
+
return new RegExp(`${escaped}\\b`, 'g');
|
|
126
|
+
} else if (/\d+(?:px|%|em|rem|vh|vw|pt|ch|ex)$/.test(value)) {
|
|
127
|
+
// Values with CSS units - ensure not part of larger number
|
|
128
|
+
return new RegExp(`(?<!\\d)${escaped}\\b`, 'g');
|
|
129
|
+
} else if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
130
|
+
// Pure numbers/decimals (unitless) - ensure not part of larger number or before units
|
|
131
|
+
// Negative lookbehind: not preceded by digit
|
|
132
|
+
// Negative lookahead: not followed by digit, unit letters, or % (for keyframe selectors)
|
|
133
|
+
return new RegExp(`(?<!\\d)${escaped}(?!\\d|[a-z%])`, 'g');
|
|
134
|
+
} else if (/^rgba?\(/.test(value)) {
|
|
135
|
+
// RGB/RGBA colors - match complete function
|
|
136
|
+
return new RegExp(escaped, 'g');
|
|
137
|
+
} else {
|
|
138
|
+
// Other values (keywords, etc.) - use word boundaries
|
|
139
|
+
return new RegExp(`\\b${escaped}\\b`, 'g');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Checks if a position in content is in a safe zone (should not be replaced)
|
|
145
|
+
*/
|
|
146
|
+
function isInSafeZone(content, position, matchedValue) {
|
|
147
|
+
const DEBUG = false; // Enable for debugging
|
|
148
|
+
|
|
149
|
+
// Check for variable declarations
|
|
150
|
+
const lineStart = content.lastIndexOf('\n', position);
|
|
151
|
+
const lineEnd = content.indexOf('\n', position);
|
|
152
|
+
const line = content.substring(lineStart, lineEnd);
|
|
153
|
+
|
|
154
|
+
// Check if this line is a variable declaration: $variable-name: value;
|
|
155
|
+
// Match pattern: optional whitespace, $, identifier, optional whitespace, colon
|
|
156
|
+
const varDeclMatch = line.match(/^\s*\$([\w-]+)\s*:/);
|
|
157
|
+
if (varDeclMatch) {
|
|
158
|
+
// This line IS a variable declaration
|
|
159
|
+
if (DEBUG) console.log(` → SAFE: Variable declaration line`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if we're in the middle of a larger number or value
|
|
164
|
+
const charBefore = content.charAt(position - 1);
|
|
165
|
+
const charAfter = content.charAt(position + matchedValue.length);
|
|
166
|
+
|
|
167
|
+
// Prevent replacement if surrounded by digits (partial number match)
|
|
168
|
+
if (/\d/.test(charBefore) || (/^\d+$/.test(matchedValue) && /\d/.test(charAfter))) {
|
|
169
|
+
if (DEBUG) console.log(` → SAFE: Surrounded by digits`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Prevent replacement within hex colors
|
|
174
|
+
if (/[0-9a-fA-F]/.test(charBefore) || /[0-9a-fA-F]/.test(charAfter)) {
|
|
175
|
+
// Look back further to check if we're in a hex color
|
|
176
|
+
const lookBehind = content.substring(Math.max(0, position - 10), position);
|
|
177
|
+
if (/#[0-9a-fA-F]*$/.test(lookBehind)) {
|
|
178
|
+
if (DEBUG) console.log(` → SAFE: Inside hex color`);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Prevent replacement if it would create invalid variable concatenation
|
|
184
|
+
if (charBefore === '$' || charAfter === '$') {
|
|
185
|
+
if (DEBUG) console.log(` → SAFE: Near $ symbol`);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Prevent replacement for negative values (e.g., -16px should not become -$spacing-md)
|
|
190
|
+
if (charBefore === '-' && /^\d/.test(matchedValue)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check for @use, @forward, @import
|
|
195
|
+
if (/@(?:use|forward|import)/.test(line)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check for url(), calc(), transform functions, and other contexts
|
|
200
|
+
const beforeContext = content.substring(Math.max(0, position - 100), position);
|
|
201
|
+
|
|
202
|
+
// Prevent replacement inside calc() expressions
|
|
203
|
+
if (/calc\s*\([^)]*$/.test(beforeContext)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Prevent replacement inside transform functions
|
|
208
|
+
if (/(?:translate|rotate|scale|skew|matrix|perspective)\s*\([^)]*$/.test(beforeContext)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (/url\s*\(\s*[^)]*$/.test(beforeContext)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for content property
|
|
217
|
+
if (/content\s*:\s*[^;]*$/.test(beforeContext)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check for interpolation #{}
|
|
222
|
+
if (/#\{[^}]*$/.test(beforeContext)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for Material functions
|
|
227
|
+
if (/mat\.\w+\([^)]*$/.test(beforeContext)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Prevent replacement inside attribute selectors
|
|
232
|
+
if (
|
|
233
|
+
/\[[^\]]*$/.test(beforeContext) &&
|
|
234
|
+
/^[^\[]*\]/.test(content.substring(position, position + 50))
|
|
235
|
+
) {
|
|
236
|
+
if (DEBUG) console.log(` → SAFE: Inside attribute selector`);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Prevent replacement in keyframe percentages (e.g., 0%, 50%, 100% in @keyframes)
|
|
241
|
+
// Check if we're in a @keyframes block and this position is before the opening brace
|
|
242
|
+
if (/@keyframes/.test(beforeContext)) {
|
|
243
|
+
// Check if we're in the selector part (before {) of a keyframe rule
|
|
244
|
+
// Get text from line start to current position and after
|
|
245
|
+
const lineBeforePos = content.substring(lineStart, position + matchedValue.length);
|
|
246
|
+
const lineAfterPos = content.substring(position + matchedValue.length, lineEnd);
|
|
247
|
+
|
|
248
|
+
// If the line has a { after this position and before that { there's a %,
|
|
249
|
+
// and we haven't passed the { yet, we're in the selector
|
|
250
|
+
if (
|
|
251
|
+
/%/.test(lineBeforePos) &&
|
|
252
|
+
/^\s*%/.test(
|
|
253
|
+
content.substring(position + matchedValue.length, position + matchedValue.length + 5)
|
|
254
|
+
)
|
|
255
|
+
) {
|
|
256
|
+
// We're right before the % in a keyframe selector
|
|
257
|
+
if (DEBUG) console.log(` → SAFE: Keyframe percentage selector`);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (/%[^{]*$/.test(lineBeforePos) && !lineBeforePos.includes('{')) {
|
|
261
|
+
// We're somewhere in a percentage before the {
|
|
262
|
+
if (DEBUG) console.log(` → SAFE: Keyframe percentage line`);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check for comments
|
|
268
|
+
const commentStart = content.lastIndexOf('/*', position);
|
|
269
|
+
const commentEnd = content.lastIndexOf('*/', position);
|
|
270
|
+
if (commentStart > commentEnd) {
|
|
271
|
+
if (DEBUG) console.log(` → SAFE: Inside block comment`);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for inline comments (//)
|
|
276
|
+
const lineContent = content.substring(lineStart, position + matchedValue.length);
|
|
277
|
+
const inlineCommentPos = lineContent.lastIndexOf('//');
|
|
278
|
+
if (inlineCommentPos !== -1) {
|
|
279
|
+
// Check if our position is after the // on this line
|
|
280
|
+
const posInLine = position - lineStart;
|
|
281
|
+
if (posInLine >= inlineCommentPos) {
|
|
282
|
+
if (DEBUG) console.log(` → SAFE: Inside inline comment`);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (DEBUG) console.log(` → NOT SAFE: Will replace`);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Replaces value at specific index
|
|
293
|
+
*/
|
|
294
|
+
function replaceAt(content, searchValue, replaceValue, startIndex) {
|
|
295
|
+
// Find the exact position and replace
|
|
296
|
+
const before = content.substring(0, startIndex);
|
|
297
|
+
const after = content.substring(startIndex);
|
|
298
|
+
|
|
299
|
+
// Replace first occurrence in 'after'
|
|
300
|
+
const replaced = after.replace(searchValue, replaceValue);
|
|
301
|
+
|
|
302
|
+
return before + replaced;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Gets relative import path from current file to variables file
|
|
307
|
+
*/
|
|
308
|
+
function getRelativeImportPath(fromFile, toFile) {
|
|
309
|
+
const fromDir = path.dirname(fromFile);
|
|
310
|
+
let relativePath = path.relative(fromDir, toFile);
|
|
311
|
+
|
|
312
|
+
// Convert to forward slashes for imports
|
|
313
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
314
|
+
|
|
315
|
+
// Remove .scss extension
|
|
316
|
+
relativePath = relativePath.replace(/\.scss$/, '');
|
|
317
|
+
|
|
318
|
+
// Add ./ prefix if not starting with ../
|
|
319
|
+
if (!relativePath.startsWith('..')) {
|
|
320
|
+
relativePath = './' + relativePath;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return relativePath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
refactorScssFiles,
|
|
328
|
+
refactorFile,
|
|
329
|
+
};
|