codescoop 1.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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/codescoop.js +276 -0
- package/package.json +75 -0
- package/src/cli/interactive.js +153 -0
- package/src/index.js +303 -0
- package/src/output/conversion-generator.js +501 -0
- package/src/output/markdown.js +562 -0
- package/src/parsers/css-analyzer.js +488 -0
- package/src/parsers/html-parser.js +455 -0
- package/src/parsers/js-analyzer.js +413 -0
- package/src/utils/file-scanner.js +191 -0
- package/src/utils/ghost-detector.js +174 -0
- package/src/utils/library-detector.js +335 -0
- package/src/utils/specificity-calculator.js +251 -0
- package/src/utils/template-parser.js +260 -0
- package/src/utils/url-fetcher.js +123 -0
- package/src/utils/validation.js +278 -0
- package/src/utils/variable-extractor.js +271 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities
|
|
3
|
+
* Handles edge cases and input validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate HTML file
|
|
11
|
+
* @param {string} filePath - Path to HTML file
|
|
12
|
+
* @returns {Object} Validation result
|
|
13
|
+
*/
|
|
14
|
+
function validateHTMLFile(filePath) {
|
|
15
|
+
const errors = [];
|
|
16
|
+
const warnings = [];
|
|
17
|
+
|
|
18
|
+
// Check if file exists
|
|
19
|
+
if (!fs.existsSync(filePath)) {
|
|
20
|
+
errors.push(`File not found: ${filePath}`);
|
|
21
|
+
return { valid: false, errors, warnings };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check file extension
|
|
25
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
26
|
+
if (!['.html', '.htm', '.xhtml', '.php', '.ejs', '.hbs', '.pug'].includes(ext)) {
|
|
27
|
+
warnings.push(`File extension "${ext}" is not a typical HTML extension. Proceeding anyway.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Try to read file
|
|
31
|
+
let content;
|
|
32
|
+
try {
|
|
33
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'EACCES') {
|
|
36
|
+
errors.push(`Permission denied: Cannot read ${filePath}`);
|
|
37
|
+
} else if (error.code === 'EISDIR') {
|
|
38
|
+
errors.push(`Path is a directory, not a file: ${filePath}`);
|
|
39
|
+
} else {
|
|
40
|
+
errors.push(`Cannot read file: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
return { valid: false, errors, warnings };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if file is empty
|
|
46
|
+
if (!content || content.trim().length === 0) {
|
|
47
|
+
errors.push('File is empty');
|
|
48
|
+
return { valid: false, errors, warnings };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if file looks like HTML (has at least some HTML-like content)
|
|
52
|
+
if (!/<[a-z][\s\S]*>/i.test(content)) {
|
|
53
|
+
warnings.push('File does not appear to contain HTML tags');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if it might be a binary file (has null bytes)
|
|
57
|
+
if (content.includes('\0')) {
|
|
58
|
+
errors.push('File appears to be binary, not text');
|
|
59
|
+
return { valid: false, errors, warnings };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check file size (warn if very large)
|
|
63
|
+
const stats = fs.statSync(filePath);
|
|
64
|
+
const sizeInMB = stats.size / (1024 * 1024);
|
|
65
|
+
if (sizeInMB > 10) {
|
|
66
|
+
warnings.push(`Large file (${sizeInMB.toFixed(2)} MB). Analysis may be slow.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: true, errors, warnings, content, sizeInMB };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate output path
|
|
74
|
+
* @param {string} outputPath - Path to output file
|
|
75
|
+
* @returns {Object} Validation result
|
|
76
|
+
*/
|
|
77
|
+
function validateOutputPath(outputPath) {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const warnings = [];
|
|
80
|
+
|
|
81
|
+
if (!outputPath) {
|
|
82
|
+
return { valid: true, errors, warnings }; // Will use default
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dir = path.dirname(outputPath);
|
|
86
|
+
|
|
87
|
+
// Check if directory exists
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
errors.push(`Cannot create output directory: ${error.message}`);
|
|
93
|
+
return { valid: false, errors, warnings };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if path is a directory
|
|
98
|
+
if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
|
|
99
|
+
errors.push(`Output path is a directory: ${outputPath}. Please specify a file name.`);
|
|
100
|
+
return { valid: false, errors, warnings };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if file already exists
|
|
104
|
+
if (fs.existsSync(outputPath)) {
|
|
105
|
+
warnings.push(`Output file already exists and will be overwritten: ${outputPath}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check write permission
|
|
109
|
+
try {
|
|
110
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
errors.push(`Cannot write to directory: ${dir}`);
|
|
113
|
+
return { valid: false, errors, warnings };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { valid: true, errors, warnings };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate project directory
|
|
121
|
+
* @param {string} dirPath - Path to project directory
|
|
122
|
+
* @returns {Object} Validation result
|
|
123
|
+
*/
|
|
124
|
+
function validateProjectDir(dirPath) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
const warnings = [];
|
|
127
|
+
|
|
128
|
+
// Check if directory exists
|
|
129
|
+
if (!fs.existsSync(dirPath)) {
|
|
130
|
+
errors.push(`Directory not found: ${dirPath}`);
|
|
131
|
+
return { valid: false, errors, warnings };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if it's actually a directory
|
|
135
|
+
if (!fs.statSync(dirPath).isDirectory()) {
|
|
136
|
+
errors.push(`Path is not a directory: ${dirPath}`);
|
|
137
|
+
return { valid: false, errors, warnings };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check read permission
|
|
141
|
+
try {
|
|
142
|
+
fs.accessSync(dirPath, fs.constants.R_OK);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
errors.push(`Cannot read directory: ${dirPath}`);
|
|
145
|
+
return { valid: false, errors, warnings };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Count files to give user an idea
|
|
149
|
+
try {
|
|
150
|
+
const entries = fs.readdirSync(dirPath);
|
|
151
|
+
if (entries.length === 0) {
|
|
152
|
+
warnings.push('Directory is empty');
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
warnings.push(`Could not read directory contents: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { valid: true, errors, warnings };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Safe file read with encoding detection
|
|
163
|
+
* @param {string} filePath - Path to file
|
|
164
|
+
* @returns {Object} Result with content or error
|
|
165
|
+
*/
|
|
166
|
+
function safeReadFile(filePath) {
|
|
167
|
+
try {
|
|
168
|
+
// Try UTF-8 first
|
|
169
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
170
|
+
|
|
171
|
+
// Check for BOM and remove if present
|
|
172
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
173
|
+
content = content.slice(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if content looks corrupted (many replacement characters)
|
|
177
|
+
const replacementChars = (content.match(/\uFFFD/g) || []).length;
|
|
178
|
+
if (replacementChars > content.length * 0.1) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: 'File encoding issue: Too many invalid characters',
|
|
182
|
+
encoding: 'unknown'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { success: true, content, encoding: 'utf-8' };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: error.message,
|
|
191
|
+
encoding: null
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Sanitize selector input
|
|
198
|
+
* Prevents potential injection or crash-causing input
|
|
199
|
+
* @param {string} selector - User-provided selector
|
|
200
|
+
* @returns {Object} Sanitized result
|
|
201
|
+
*/
|
|
202
|
+
function sanitizeSelector(selector) {
|
|
203
|
+
if (!selector || typeof selector !== 'string') {
|
|
204
|
+
return { valid: false, error: 'Selector must be a non-empty string' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const trimmed = selector.trim();
|
|
208
|
+
|
|
209
|
+
// Max length check
|
|
210
|
+
if (trimmed.length > 1000) {
|
|
211
|
+
return { valid: false, error: 'Selector is too long (max 1000 characters)' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for potentially problematic patterns
|
|
215
|
+
const dangerousPatterns = [
|
|
216
|
+
/javascript:/i,
|
|
217
|
+
/<script/i,
|
|
218
|
+
/on\w+=/i,
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
for (const pattern of dangerousPatterns) {
|
|
222
|
+
if (pattern.test(trimmed)) {
|
|
223
|
+
return { valid: false, error: 'Selector contains invalid patterns' };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { valid: true, selector: trimmed };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if a path is safe (no directory traversal attacks)
|
|
232
|
+
* @param {string} basePath - Base directory
|
|
233
|
+
* @param {string} targetPath - Target path to validate
|
|
234
|
+
* @returns {boolean} True if safe
|
|
235
|
+
*/
|
|
236
|
+
function isPathSafe(basePath, targetPath) {
|
|
237
|
+
const resolvedBase = path.resolve(basePath);
|
|
238
|
+
const resolvedTarget = path.resolve(basePath, targetPath);
|
|
239
|
+
|
|
240
|
+
return resolvedTarget.startsWith(resolvedBase);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format error message for user display
|
|
245
|
+
* @param {Error} error - Error object
|
|
246
|
+
* @param {boolean} verbose - Show stack trace
|
|
247
|
+
* @returns {string} Formatted message
|
|
248
|
+
*/
|
|
249
|
+
function formatError(error, verbose = false) {
|
|
250
|
+
let message = error.message || 'Unknown error occurred';
|
|
251
|
+
|
|
252
|
+
// Add helpful context for common errors
|
|
253
|
+
if (error.code === 'ENOENT') {
|
|
254
|
+
message = `File or directory not found: ${error.path || 'unknown'}`;
|
|
255
|
+
} else if (error.code === 'EACCES') {
|
|
256
|
+
message = `Permission denied: ${error.path || 'unknown'}`;
|
|
257
|
+
} else if (error.code === 'EMFILE') {
|
|
258
|
+
message = 'Too many files open. Try closing some applications.';
|
|
259
|
+
} else if (error.code === 'ENOSPC') {
|
|
260
|
+
message = 'Disk is full. Free up some space.';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (verbose && error.stack) {
|
|
264
|
+
message += `\n\nStack trace:\n${error.stack}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return message;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
validateHTMLFile,
|
|
272
|
+
validateOutputPath,
|
|
273
|
+
validateProjectDir,
|
|
274
|
+
safeReadFile,
|
|
275
|
+
sanitizeSelector,
|
|
276
|
+
isPathSafe,
|
|
277
|
+
formatError
|
|
278
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable Extractor Module
|
|
3
|
+
* Extracts CSS custom properties and SCSS variables definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract all CSS custom property usages from CSS content
|
|
11
|
+
* @param {string} content - CSS/SCSS content
|
|
12
|
+
* @returns {Set<string>} Set of variable names used
|
|
13
|
+
*/
|
|
14
|
+
function extractCSSVariableUsages(content) {
|
|
15
|
+
const usages = new Set();
|
|
16
|
+
|
|
17
|
+
// Match var(--variable-name) patterns
|
|
18
|
+
const varRegex = /var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,\s*[^)]+)?\)/g;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
21
|
+
usages.add(match[1]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return usages;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract all SCSS variable usages from SCSS content
|
|
29
|
+
* @param {string} content - SCSS content
|
|
30
|
+
* @returns {Set<string>} Set of variable names used
|
|
31
|
+
*/
|
|
32
|
+
function extractSCSSVariableUsages(content) {
|
|
33
|
+
const usages = new Set();
|
|
34
|
+
|
|
35
|
+
// Match $variable-name patterns (but not in definitions)
|
|
36
|
+
// This regex looks for $ followed by variable name that's NOT at the start of a definition
|
|
37
|
+
const scssVarRegex = /(?<!^\s*)\$([a-zA-Z_][a-zA-Z0-9_-]*)/gm;
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = scssVarRegex.exec(content)) !== null) {
|
|
40
|
+
usages.add('$' + match[1]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return usages;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find CSS custom property definitions in content
|
|
48
|
+
* @param {string} content - CSS/SCSS content
|
|
49
|
+
* @param {Set<string>} variableNames - Variables to find
|
|
50
|
+
* @returns {Object} Map of variable name to definition
|
|
51
|
+
*/
|
|
52
|
+
function findCSSVariableDefinitions(content, variableNames) {
|
|
53
|
+
const definitions = {};
|
|
54
|
+
|
|
55
|
+
for (const varName of variableNames) {
|
|
56
|
+
// Look for --variable-name: value; pattern
|
|
57
|
+
const escapedName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
const defRegex = new RegExp(`(${escapedName})\\s*:\\s*([^;]+);`, 'gm');
|
|
59
|
+
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = defRegex.exec(content)) !== null) {
|
|
62
|
+
// Get some context around the match
|
|
63
|
+
const lineStart = content.lastIndexOf('\n', match.index) + 1;
|
|
64
|
+
const lineEnd = content.indexOf('\n', match.index + match[0].length);
|
|
65
|
+
const line = content.substring(lineStart, lineEnd > -1 ? lineEnd : undefined).trim();
|
|
66
|
+
|
|
67
|
+
// Try to find the selector/context
|
|
68
|
+
const beforeMatch = content.substring(Math.max(0, match.index - 200), match.index);
|
|
69
|
+
const selectorMatch = beforeMatch.match(/([^{}]+)\s*\{[^{}]*$/);
|
|
70
|
+
const selector = selectorMatch ? selectorMatch[1].trim().split('\n').pop().trim() : ':root';
|
|
71
|
+
|
|
72
|
+
if (!definitions[varName]) {
|
|
73
|
+
definitions[varName] = [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
definitions[varName].push({
|
|
77
|
+
value: match[2].trim(),
|
|
78
|
+
context: selector,
|
|
79
|
+
fullLine: line
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return definitions;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find SCSS variable definitions in content
|
|
89
|
+
* @param {string} content - SCSS content
|
|
90
|
+
* @param {Set<string>} variableNames - Variables to find (with $ prefix)
|
|
91
|
+
* @returns {Object} Map of variable name to definition
|
|
92
|
+
*/
|
|
93
|
+
function findSCSSVariableDefinitions(content, variableNames) {
|
|
94
|
+
const definitions = {};
|
|
95
|
+
|
|
96
|
+
for (const varName of variableNames) {
|
|
97
|
+
// Look for $variable-name: value; pattern at the start of a line
|
|
98
|
+
const escapedName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
const defRegex = new RegExp(`^\\s*(${escapedName})\\s*:\\s*([^;]+);`, 'gm');
|
|
100
|
+
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = defRegex.exec(content)) !== null) {
|
|
103
|
+
const line = match[0].trim();
|
|
104
|
+
|
|
105
|
+
if (!definitions[varName]) {
|
|
106
|
+
definitions[varName] = [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
definitions[varName].push({
|
|
110
|
+
value: match[2].trim(),
|
|
111
|
+
context: 'global',
|
|
112
|
+
fullLine: line
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return definitions;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract variables from matched CSS rules and find their definitions
|
|
122
|
+
* @param {Array} cssMatches - Array of CSS match objects with content
|
|
123
|
+
* @param {Array} projectFiles - Array of CSS/SCSS file paths
|
|
124
|
+
* @param {string} projectDir - Project directory
|
|
125
|
+
* @returns {Object} Variable definitions found
|
|
126
|
+
*/
|
|
127
|
+
async function extractVariablesFromMatches(cssMatches, projectFiles, projectDir) {
|
|
128
|
+
// Collect all variable usages from matched rules
|
|
129
|
+
const cssVarsUsed = new Set();
|
|
130
|
+
const scssVarsUsed = new Set();
|
|
131
|
+
|
|
132
|
+
for (const match of cssMatches) {
|
|
133
|
+
const content = match.content || '';
|
|
134
|
+
|
|
135
|
+
// Extract CSS custom property usages
|
|
136
|
+
const cssUsages = extractCSSVariableUsages(content);
|
|
137
|
+
cssUsages.forEach(v => cssVarsUsed.add(v));
|
|
138
|
+
|
|
139
|
+
// Extract SCSS variable usages
|
|
140
|
+
const scssUsages = extractSCSSVariableUsages(content);
|
|
141
|
+
scssUsages.forEach(v => scssVarsUsed.add(v));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (cssVarsUsed.size === 0 && scssVarsUsed.size === 0) {
|
|
145
|
+
return { cssVariables: {}, scssVariables: {}, usedVariables: [] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Search project files for definitions
|
|
149
|
+
const allCSSDefinitions = {};
|
|
150
|
+
const allSCSSDefinitions = {};
|
|
151
|
+
|
|
152
|
+
const cssFiles = projectFiles.filter(f =>
|
|
153
|
+
f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
for (const filePath of cssFiles) {
|
|
157
|
+
try {
|
|
158
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
159
|
+
const relPath = path.relative(projectDir, filePath);
|
|
160
|
+
|
|
161
|
+
// Find CSS variable definitions
|
|
162
|
+
if (cssVarsUsed.size > 0) {
|
|
163
|
+
const defs = findCSSVariableDefinitions(content, cssVarsUsed);
|
|
164
|
+
for (const [varName, defList] of Object.entries(defs)) {
|
|
165
|
+
if (!allCSSDefinitions[varName]) {
|
|
166
|
+
allCSSDefinitions[varName] = [];
|
|
167
|
+
}
|
|
168
|
+
defList.forEach(def => {
|
|
169
|
+
allCSSDefinitions[varName].push({
|
|
170
|
+
...def,
|
|
171
|
+
file: relPath
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find SCSS variable definitions
|
|
178
|
+
if (scssVarsUsed.size > 0 && (filePath.endsWith('.scss') || filePath.endsWith('.sass'))) {
|
|
179
|
+
const defs = findSCSSVariableDefinitions(content, scssVarsUsed);
|
|
180
|
+
for (const [varName, defList] of Object.entries(defs)) {
|
|
181
|
+
if (!allSCSSDefinitions[varName]) {
|
|
182
|
+
allSCSSDefinitions[varName] = [];
|
|
183
|
+
}
|
|
184
|
+
defList.forEach(def => {
|
|
185
|
+
allSCSSDefinitions[varName].push({
|
|
186
|
+
...def,
|
|
187
|
+
file: relPath
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Skip files that can't be read
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find undefined variables (used but not found)
|
|
198
|
+
const undefinedCSSVars = [...cssVarsUsed].filter(v => !allCSSDefinitions[v]);
|
|
199
|
+
const undefinedSCSSVars = [...scssVarsUsed].filter(v => !allSCSSDefinitions[v]);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
cssVariables: allCSSDefinitions,
|
|
203
|
+
scssVariables: allSCSSDefinitions,
|
|
204
|
+
undefinedCSSVariables: undefinedCSSVars,
|
|
205
|
+
undefinedSCSSVariables: undefinedSCSSVars,
|
|
206
|
+
usedVariables: [...cssVarsUsed, ...scssVarsUsed]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format variable definitions as markdown
|
|
212
|
+
* @param {Object} variableData - Variable data from extractVariablesFromMatches
|
|
213
|
+
* @returns {string} Markdown content
|
|
214
|
+
*/
|
|
215
|
+
function formatVariablesAsMarkdown(variableData) {
|
|
216
|
+
const { cssVariables, scssVariables, undefinedCSSVariables, undefinedSCSSVariables } = variableData;
|
|
217
|
+
|
|
218
|
+
let content = '';
|
|
219
|
+
|
|
220
|
+
// CSS Variables
|
|
221
|
+
const cssVarNames = Object.keys(cssVariables);
|
|
222
|
+
if (cssVarNames.length > 0) {
|
|
223
|
+
content += `### CSS Custom Properties Used\n\n`;
|
|
224
|
+
content += `| Variable | Value | Context | File |\n`;
|
|
225
|
+
content += `|----------|-------|---------|------|\n`;
|
|
226
|
+
|
|
227
|
+
for (const varName of cssVarNames) {
|
|
228
|
+
const defs = cssVariables[varName];
|
|
229
|
+
// Show first definition (usually :root)
|
|
230
|
+
const def = defs[0];
|
|
231
|
+
const shortValue = def.value.length > 40 ? def.value.substring(0, 37) + '...' : def.value;
|
|
232
|
+
content += `| \`${varName}\` | \`${shortValue}\` | ${def.context} | \`${def.file}\` |\n`;
|
|
233
|
+
}
|
|
234
|
+
content += '\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// SCSS Variables
|
|
238
|
+
const scssVarNames = Object.keys(scssVariables);
|
|
239
|
+
if (scssVarNames.length > 0) {
|
|
240
|
+
content += `### SCSS Variables Used\n\n`;
|
|
241
|
+
content += `\`\`\`scss\n`;
|
|
242
|
+
for (const varName of scssVarNames) {
|
|
243
|
+
const defs = scssVariables[varName];
|
|
244
|
+
const def = defs[0];
|
|
245
|
+
content += `${varName}: ${def.value};\n`;
|
|
246
|
+
}
|
|
247
|
+
content += `\`\`\`\n\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Undefined variables warning
|
|
251
|
+
const allUndefined = [...(undefinedCSSVariables || []), ...(undefinedSCSSVariables || [])];
|
|
252
|
+
if (allUndefined.length > 0) {
|
|
253
|
+
content += `### ⚠️ Undefined Variables\n\n`;
|
|
254
|
+
content += `The following variables are used but their definitions were not found:\n\n`;
|
|
255
|
+
allUndefined.forEach(v => {
|
|
256
|
+
content += `- \`${v}\`\n`;
|
|
257
|
+
});
|
|
258
|
+
content += `\n> These may be defined in a file not scanned, or loaded via a CSS framework.\n\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return content;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
extractCSSVariableUsages,
|
|
266
|
+
extractSCSSVariableUsages,
|
|
267
|
+
findCSSVariableDefinitions,
|
|
268
|
+
findSCSSVariableDefinitions,
|
|
269
|
+
extractVariablesFromMatches,
|
|
270
|
+
formatVariablesAsMarkdown
|
|
271
|
+
};
|