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,153 @@
1
+ /**
2
+ * Interactive CLI Mode
3
+ * Shows HTML structure and lets user select a component
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const chalk = require('chalk');
8
+ const fs = require('fs');
9
+ const { parseHTML, getHTMLStructure } = require('../parsers/html-parser');
10
+
11
+ /**
12
+ * Run interactive mode to select a component
13
+ * @param {string} htmlPath - Path to HTML file
14
+ * @returns {Object|null} Selected component info or null if cancelled
15
+ */
16
+ async function runInteractiveMode(htmlPath) {
17
+ // Parse HTML file
18
+ const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
19
+ const $ = parseHTML(htmlContent);
20
+
21
+ // Get structure
22
+ const structure = getHTMLStructure($);
23
+
24
+ if (structure.length === 0) {
25
+ console.log(chalk.yellow('No significant HTML elements found in the file.'));
26
+ return null;
27
+ }
28
+
29
+ // Display structure
30
+ console.log(chalk.cyan('┌─────────────────────────────────────────────────────────────┐'));
31
+ console.log(chalk.cyan('│') + chalk.bold.white(' HTML Structure ') + chalk.cyan('│'));
32
+ console.log(chalk.cyan('├─────────────────────────────────────────────────────────────┤'));
33
+
34
+ structure.forEach(item => {
35
+ const indexStr = chalk.gray(`[${String(item.index).padStart(2)}]`);
36
+ const display = item.isChild
37
+ ? chalk.gray(item.display)
38
+ : chalk.white(item.display);
39
+ console.log(chalk.cyan('│') + ` ${indexStr} ${display}`.padEnd(60) + chalk.cyan('│'));
40
+ });
41
+
42
+ console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
43
+ console.log('');
44
+
45
+ // Build choices for inquirer
46
+ const choices = structure.map(item => ({
47
+ name: `${item.display}`,
48
+ value: item.selector,
49
+ short: item.selector
50
+ }));
51
+
52
+ // Add option to enter custom selector
53
+ choices.push(new inquirer.Separator());
54
+ choices.push({
55
+ name: chalk.yellow('Enter custom selector...'),
56
+ value: '__custom__'
57
+ });
58
+ choices.push({
59
+ name: chalk.gray('Cancel'),
60
+ value: '__cancel__'
61
+ });
62
+
63
+ // Prompt for selection
64
+ const { selection } = await inquirer.prompt([
65
+ {
66
+ type: 'list',
67
+ name: 'selection',
68
+ message: 'Select a component to analyze:',
69
+ choices,
70
+ pageSize: 15
71
+ }
72
+ ]);
73
+
74
+ if (selection === '__cancel__') {
75
+ return null;
76
+ }
77
+
78
+ if (selection === '__custom__') {
79
+ const { customSelector } = await inquirer.prompt([
80
+ {
81
+ type: 'input',
82
+ name: 'customSelector',
83
+ message: 'Enter CSS selector:',
84
+ validate: (input) => {
85
+ if (!input.trim()) {
86
+ return 'Please enter a valid CSS selector';
87
+ }
88
+ // Basic validation - try to use it
89
+ try {
90
+ $(input);
91
+ return true;
92
+ } catch (e) {
93
+ return 'Invalid CSS selector';
94
+ }
95
+ }
96
+ }
97
+ ]);
98
+
99
+ return {
100
+ selector: customSelector,
101
+ mode: 'custom'
102
+ };
103
+ }
104
+
105
+ return {
106
+ selector: selection,
107
+ mode: 'selected'
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Display a preview of what will be analyzed
113
+ */
114
+ async function showPreview($, selector) {
115
+ const element = $(selector).first();
116
+
117
+ if (element.length === 0) {
118
+ console.log(chalk.red(`No element found matching: ${selector}`));
119
+ return false;
120
+ }
121
+
122
+ const classes = element.attr('class')?.split(/\s+/).filter(c => c) || [];
123
+ const id = element.attr('id');
124
+ const tagName = element.prop('tagName')?.toLowerCase();
125
+ const childCount = element.find('*').length;
126
+
127
+ console.log('');
128
+ console.log(chalk.cyan('Component Preview:'));
129
+ console.log(chalk.gray('─'.repeat(40)));
130
+ console.log(` Tag: ${chalk.white(tagName)}`);
131
+ if (id) console.log(` ID: ${chalk.yellow('#' + id)}`);
132
+ if (classes.length > 0) console.log(` Classes: ${chalk.green(classes.map(c => '.' + c).join(' '))}`);
133
+ console.log(` Children: ${chalk.gray(childCount + ' elements')}`);
134
+ console.log(chalk.gray('─'.repeat(40)));
135
+ console.log('');
136
+
137
+ // Confirm
138
+ const { confirmed } = await inquirer.prompt([
139
+ {
140
+ type: 'confirm',
141
+ name: 'confirmed',
142
+ message: 'Analyze this component?',
143
+ default: true
144
+ }
145
+ ]);
146
+
147
+ return confirmed;
148
+ }
149
+
150
+ module.exports = {
151
+ runInteractiveMode,
152
+ showPreview
153
+ };
package/src/index.js ADDED
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Main orchestrator for html-scan
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { parseHTML, extractTargetElement } = require('./parsers/html-parser');
8
+ const { analyzeCSS } = require('./parsers/css-analyzer');
9
+ const { analyzeJS } = require('./parsers/js-analyzer');
10
+ const { findProjectFiles, getLinkedFiles } = require('./utils/file-scanner');
11
+ const { generateMarkdown } = require('./output/markdown');
12
+ const {
13
+ detectLibrariesFromPaths,
14
+ detectLibrariesFromHTML,
15
+ detectLibrariesFromClasses,
16
+ isLibraryFile
17
+ } = require('./utils/library-detector');
18
+ const { extractVariablesFromMatches } = require('./utils/variable-extractor');
19
+
20
+ /**
21
+ * Run the full analysis
22
+ * @param {Object} options
23
+ * @param {string} options.htmlPath - Path to HTML file
24
+ * @param {string} options.projectDir - Project directory to scan
25
+ * @param {string} options.selector - CSS selector to target
26
+ * @param {string} options.lineRange - Line range (e.g., "45-80")
27
+ * @param {string} options.outputPath - Output file path
28
+ * @param {boolean} options.includeInline - Include inline styles/scripts
29
+ * @param {boolean} options.verbose - Verbose logging
30
+ */
31
+ async function runAnalysis(options) {
32
+ const {
33
+ htmlPath,
34
+ projectDir,
35
+ selector,
36
+ lineRange,
37
+ matchIndex = 0,
38
+ outputPath,
39
+ includeInline = true,
40
+ verbose = false,
41
+ // Compact mode options
42
+ compact = false,
43
+ forConversion = false,
44
+ maxRulesPerFile = 20,
45
+ maxJsPerFile = 10,
46
+ summaryOnly = false,
47
+ skipMinified = false,
48
+ // URL/Template mode support
49
+ htmlContent: preloadedContent = null,
50
+ sourceType = 'file'
51
+ } = options;
52
+
53
+ const log = verbose ? console.log : () => { };
54
+
55
+ // Step 1: Parse HTML and extract target element
56
+ log('Parsing HTML file...');
57
+
58
+ // Use pre-loaded content (from URL/template) or read from file
59
+ let htmlContent;
60
+ if (preloadedContent) {
61
+ htmlContent = preloadedContent;
62
+ log(`Using pre-loaded HTML content (${sourceType} mode)`);
63
+ } else {
64
+ htmlContent = fs.readFileSync(htmlPath, 'utf-8');
65
+ }
66
+
67
+ const parsedHTML = parseHTML(htmlContent);
68
+
69
+ const targetInfo = extractTargetElement(parsedHTML, htmlContent, {
70
+ selector,
71
+ lineRange,
72
+ matchIndex
73
+ });
74
+
75
+ log(`Target element: ${targetInfo.summary}`);
76
+ log(`Classes: ${targetInfo.classes.join(', ') || 'none'}`);
77
+ log(`IDs: ${targetInfo.ids.join(', ') || 'none'}`);
78
+
79
+ // Step 2: Find all CSS/JS files in project (skip if no projectDir for URL mode)
80
+ log('\nScanning project for CSS/JS files...');
81
+ let projectFiles = { css: [], js: [] };
82
+ if (projectDir) {
83
+ projectFiles = await findProjectFiles(projectDir);
84
+ }
85
+ log(`Found ${projectFiles.css.length} CSS files, ${projectFiles.js.length} JS files`);
86
+
87
+ // Step 3: Get files that are actually linked in HTML
88
+ const linkedFiles = getLinkedFiles(parsedHTML, htmlPath);
89
+ log(`Linked in HTML: ${linkedFiles.css.length} CSS, ${linkedFiles.js.length} JS`);
90
+
91
+ // Step 4: Detect libraries
92
+ log('\nDetecting libraries...');
93
+ const allFiles = [...projectFiles.css, ...projectFiles.js];
94
+ const librariesFromFiles = detectLibrariesFromPaths(allFiles);
95
+ const librariesFromCDN = detectLibrariesFromHTML(parsedHTML);
96
+ const librariesFromClasses = detectLibrariesFromClasses(targetInfo.classes);
97
+
98
+ // Combine library info
99
+ const detectedLibraries = {
100
+ fromFiles: librariesFromFiles,
101
+ fromCDN: librariesFromCDN,
102
+ fromClasses: librariesFromClasses
103
+ };
104
+ log(`Detected ${Object.keys(librariesFromFiles).length} libraries from files`);
105
+ log(`Detected ${librariesFromCDN.length} libraries from CDN`);
106
+
107
+ // Step 5: Analyze all CSS files (separating libraries from custom code)
108
+ log('\nAnalyzing CSS files...');
109
+ const cssResults = [];
110
+ const cssLibraryResults = [];
111
+
112
+ for (const cssFile of projectFiles.css) {
113
+ const libInfo = isLibraryFile(cssFile);
114
+ const result = await analyzeCSS(cssFile, targetInfo, { verbose });
115
+
116
+ if (result.matches.length > 0) {
117
+ result.isLinked = linkedFiles.css.some(f =>
118
+ path.resolve(f) === path.resolve(cssFile)
119
+ );
120
+ result.isLibrary = !!libInfo;
121
+ result.libraryName = libInfo?.name || null;
122
+
123
+ if (libInfo) {
124
+ cssLibraryResults.push(result);
125
+ } else {
126
+ cssResults.push(result);
127
+ }
128
+ }
129
+ }
130
+
131
+ // Step 6: Analyze inline styles if requested
132
+ let inlineStyles = [];
133
+ if (includeInline) {
134
+ log('Extracting inline styles...');
135
+ inlineStyles = extractInlineStyles(parsedHTML, targetInfo);
136
+ }
137
+
138
+ // Step 7: Analyze all JS files (separating libraries from custom code)
139
+ log('\nAnalyzing JavaScript files...');
140
+ const jsResults = [];
141
+ const jsLibraryResults = [];
142
+
143
+ for (const jsFile of projectFiles.js) {
144
+ const libInfo = isLibraryFile(jsFile);
145
+ const result = await analyzeJS(jsFile, targetInfo, { verbose });
146
+
147
+ if (result.matches.length > 0) {
148
+ result.isLinked = linkedFiles.js.some(f =>
149
+ path.resolve(f) === path.resolve(jsFile)
150
+ );
151
+ result.isLibrary = !!libInfo;
152
+ result.libraryName = libInfo?.name || null;
153
+
154
+ if (libInfo) {
155
+ jsLibraryResults.push(result);
156
+ } else {
157
+ jsResults.push(result);
158
+ }
159
+ }
160
+ }
161
+
162
+ // Step 8: Analyze inline scripts if requested
163
+ let inlineScripts = [];
164
+ if (includeInline) {
165
+ log('Extracting inline scripts...');
166
+ inlineScripts = extractInlineScripts(parsedHTML, targetInfo);
167
+ }
168
+
169
+ // Step 9: Identify missing imports (only for custom files, not libraries)
170
+ const missingImports = [
171
+ ...cssResults.filter(r => !r.isLinked).map(r => r.filePath),
172
+ ...jsResults.filter(r => !r.isLinked).map(r => r.filePath)
173
+ ];
174
+
175
+ // Step 10: Extract CSS/SCSS variable definitions
176
+ log('Extracting variable definitions...');
177
+ const allCSSMatches = [
178
+ ...cssResults.flatMap(r => r.matches || []),
179
+ ...inlineStyles.map(s => ({ content: s.content }))
180
+ ];
181
+ const allCSSFiles = [...projectFiles.css];
182
+ const variableData = await extractVariablesFromMatches(allCSSMatches, allCSSFiles, projectDir);
183
+ log(`Found ${variableData.usedVariables.length} variables used`);
184
+
185
+ // Step 11: Generate markdown output
186
+ log('\nGenerating markdown report...');
187
+ const analysis = {
188
+ targetInfo,
189
+ htmlPath,
190
+ projectDir,
191
+ cssResults,
192
+ jsResults,
193
+ cssLibraryResults,
194
+ jsLibraryResults,
195
+ detectedLibraries,
196
+ inlineStyles,
197
+ inlineScripts,
198
+ missingImports,
199
+ variableData,
200
+ generatedAt: new Date().toISOString(),
201
+ // Output options
202
+ outputOptions: {
203
+ compact,
204
+ forConversion,
205
+ maxRulesPerFile,
206
+ maxJsPerFile,
207
+ summaryOnly,
208
+ skipMinified
209
+ }
210
+ };
211
+
212
+ const markdown = generateMarkdown(analysis);
213
+
214
+ // Determine output path
215
+ const finalOutputPath = outputPath || generateOutputPath(targetInfo, htmlPath, projectDir);
216
+ fs.writeFileSync(finalOutputPath, markdown, 'utf-8');
217
+
218
+ return {
219
+ outputPath: finalOutputPath,
220
+ cssMatches: cssResults.reduce((sum, r) => sum + r.matches.length, 0),
221
+ jsMatches: jsResults.reduce((sum, r) => sum + r.matches.length, 0),
222
+ missingImports,
223
+ libraryCount: Object.keys(librariesFromFiles).length + librariesFromCDN.length
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Extract inline <style> blocks that affect the target
229
+ */
230
+ function extractInlineStyles(parsedHTML, targetInfo) {
231
+ const $ = parsedHTML;
232
+ const results = [];
233
+
234
+ $('style').each((index, element) => {
235
+ const content = $(element).html();
236
+ if (content) {
237
+ // Check if any target classes/IDs are referenced
238
+ const isRelevant = [...targetInfo.classes, ...targetInfo.ids, targetInfo.tagName]
239
+ .some(identifier => content.includes(identifier));
240
+
241
+ if (isRelevant) {
242
+ results.push({
243
+ index,
244
+ content: content.trim(),
245
+ type: 'inline-style'
246
+ });
247
+ }
248
+ }
249
+ });
250
+
251
+ return results;
252
+ }
253
+
254
+ /**
255
+ * Extract inline <script> blocks that reference the target
256
+ */
257
+ function extractInlineScripts(parsedHTML, targetInfo) {
258
+ const $ = parsedHTML;
259
+ const results = [];
260
+
261
+ $('script:not([src])').each((index, element) => {
262
+ const content = $(element).html();
263
+ if (content) {
264
+ // Check if any target classes/IDs are referenced
265
+ const isRelevant = [...targetInfo.classes, ...targetInfo.ids]
266
+ .some(identifier => content.includes(identifier));
267
+
268
+ if (isRelevant) {
269
+ results.push({
270
+ index,
271
+ content: content.trim(),
272
+ type: 'inline-script'
273
+ });
274
+ }
275
+ }
276
+ });
277
+
278
+ return results;
279
+ }
280
+
281
+ /**
282
+ * Generate output path based on target info
283
+ */
284
+ function generateOutputPath(targetInfo, htmlPath, projectDir = null) {
285
+ // For URLs, use projectDir or current working directory
286
+ let outputDir;
287
+ if (htmlPath.startsWith('http://') || htmlPath.startsWith('https://')) {
288
+ outputDir = projectDir || process.cwd();
289
+ } else {
290
+ outputDir = path.dirname(htmlPath);
291
+ }
292
+
293
+ const baseName = targetInfo.ids[0]
294
+ || targetInfo.classes[0]
295
+ || targetInfo.tagName
296
+ || 'component';
297
+
298
+ // Clean up the name for use as filename
299
+ const cleanName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-');
300
+ return path.join(outputDir, `${cleanName}-analysis.md`);
301
+ }
302
+
303
+ module.exports = { runAnalysis };