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.
@@ -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
+ };