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,562 @@
1
+ /**
2
+ * Markdown Output Generator
3
+ * Generates LLM-ready markdown report from analysis results
4
+ */
5
+
6
+ const path = require('path');
7
+ const { formatVariablesAsMarkdown } = require('../utils/variable-extractor');
8
+ const { detectGhostClasses, formatGhostClassesMarkdown } = require('../utils/ghost-detector');
9
+ const { analyzeConflicts, formatConflictsMarkdown } = require('../utils/specificity-calculator');
10
+
11
+ /**
12
+ * Generate markdown report from analysis
13
+ * @param {Object} analysis - Analysis results
14
+ * @returns {string} Markdown content
15
+ */
16
+ function generateMarkdown(analysis) {
17
+ const {
18
+ targetInfo,
19
+ htmlPath,
20
+ projectDir,
21
+ cssResults,
22
+ jsResults,
23
+ cssLibraryResults = [],
24
+ jsLibraryResults = [],
25
+ detectedLibraries = {},
26
+ inlineStyles,
27
+ inlineScripts,
28
+ missingImports,
29
+ variableData = {},
30
+ generatedAt,
31
+ outputOptions = {}
32
+ } = analysis;
33
+
34
+ // Extract output options with defaults
35
+ const {
36
+ compact = false,
37
+ forConversion = false,
38
+ maxRulesPerFile = compact ? 20 : Infinity,
39
+ maxJsPerFile = compact ? 10 : Infinity,
40
+ summaryOnly = false,
41
+ skipMinified = false
42
+ } = outputOptions;
43
+
44
+ // If forConversion mode, use specialized conversion context generator
45
+ if (forConversion) {
46
+ const { generateConversionContext } = require('./conversion-generator');
47
+ return generateConversionContext(analysis);
48
+ }
49
+
50
+ // Apply filters based on options
51
+ let filteredCssResults = cssResults;
52
+ let filteredJsResults = jsResults;
53
+
54
+ if (skipMinified) {
55
+ filteredCssResults = cssResults.filter(r => !r.isMinified);
56
+ filteredJsResults = jsResults.filter(r => !r.isMinified);
57
+ }
58
+
59
+ // Apply limits per file
60
+ if (compact || maxRulesPerFile < Infinity) {
61
+ filteredCssResults = filteredCssResults.map(r => ({
62
+ ...r,
63
+ matches: r.matches.slice(0, maxRulesPerFile),
64
+ truncated: r.matches.length > maxRulesPerFile,
65
+ originalCount: r.matches.length
66
+ }));
67
+ }
68
+
69
+ if (compact || maxJsPerFile < Infinity) {
70
+ filteredJsResults = filteredJsResults.map(r => ({
71
+ ...r,
72
+ matches: r.matches.slice(0, maxJsPerFile),
73
+ truncated: r.matches.length > maxJsPerFile,
74
+ originalCount: r.matches.length
75
+ }));
76
+ }
77
+
78
+ const sections = [];
79
+
80
+ // Header
81
+ sections.push(generateHeader(targetInfo, htmlPath, generatedAt, compact));
82
+
83
+ // Libraries section (NEW!)
84
+ const hasLibraries =
85
+ Object.keys(detectedLibraries.fromFiles || {}).length > 0 ||
86
+ (detectedLibraries.fromCDN || []).length > 0 ||
87
+ (detectedLibraries.fromClasses || []).length > 0;
88
+
89
+ if (hasLibraries) {
90
+ sections.push(generateLibrariesSection(detectedLibraries, cssLibraryResults, jsLibraryResults, projectDir));
91
+ }
92
+
93
+ // Missing imports warning (if any)
94
+ if (missingImports.length > 0) {
95
+ sections.push(generateMissingImportsWarning(missingImports, projectDir));
96
+ }
97
+
98
+ // In summary-only mode, skip detailed code blocks
99
+ if (summaryOnly) {
100
+ sections.push(generateSummaryOnlySection(filteredCssResults, filteredJsResults, projectDir));
101
+ sections.push(generateSummary(cssResults, jsResults, inlineStyles, inlineScripts, missingImports, detectedLibraries));
102
+ return sections.join('\n\n---\n\n');
103
+ }
104
+
105
+ // Target HTML
106
+ sections.push(generateHTMLSection(targetInfo));
107
+
108
+ // CSS/SCSS Variables section
109
+ if (variableData && variableData.usedVariables && variableData.usedVariables.length > 0) {
110
+ const varsSection = `## 🎨 CSS/SCSS Variables\n\n> These variables are used in the matched CSS rules. Definitions are included for context.\n\n${formatVariablesAsMarkdown(variableData)}`;
111
+ sections.push(varsSection);
112
+ }
113
+
114
+ // CSS Houdini @property definitions
115
+ const houdiniSection = generateHoudiniSection(cssResults, projectDir);
116
+ if (houdiniSection) {
117
+ sections.push(houdiniSection);
118
+ }
119
+
120
+ // CSS Dependencies (custom code only)
121
+ const linkedCSS = filteredCssResults.filter(r => r.isLinked);
122
+ const unlinkedCSS = filteredCssResults.filter(r => !r.isLinked);
123
+
124
+ if (linkedCSS.length > 0 || inlineStyles.length > 0) {
125
+ sections.push(generateCSSSection(linkedCSS, inlineStyles, projectDir, 'Custom CSS Dependencies', compact));
126
+ }
127
+
128
+ if (unlinkedCSS.length > 0) {
129
+ sections.push(generateCSSSection(unlinkedCSS, [], projectDir, '⚠️ Custom CSS Files NOT Linked', compact));
130
+ }
131
+
132
+ // Shadow DOM styles
133
+ const shadowDOMSection = generateShadowDOMSection(cssResults, projectDir);
134
+ if (shadowDOMSection) {
135
+ sections.push(shadowDOMSection);
136
+ }
137
+
138
+ // JavaScript References (custom code only)
139
+ const linkedJS = filteredJsResults.filter(r => r.isLinked);
140
+ const unlinkedJS = filteredJsResults.filter(r => !r.isLinked);
141
+
142
+ if (linkedJS.length > 0 || inlineScripts.length > 0) {
143
+ sections.push(generateJSSection(linkedJS, inlineScripts, projectDir, 'Custom JavaScript References', compact));
144
+ }
145
+
146
+ if (unlinkedJS.length > 0) {
147
+ sections.push(generateJSSection(unlinkedJS, [], projectDir, '⚠️ Custom JS Files NOT Linked', compact));
148
+ }
149
+
150
+ // CSS Conflict Detection (specificity analysis)
151
+ const conflicts = analyzeConflicts(cssResults, { css: cssResults.filter(r => r.isLinked).map(r => r.file) });
152
+ const conflictsMarkdown = formatConflictsMarkdown(conflicts);
153
+ if (conflictsMarkdown) {
154
+ sections.push(conflictsMarkdown);
155
+ }
156
+
157
+ // Ghost Classes Detection
158
+ const ghostData = detectGhostClasses(targetInfo, cssResults, cssLibraryResults, inlineStyles);
159
+ if (ghostData.hasGhosts) {
160
+ sections.push(formatGhostClassesMarkdown(ghostData));
161
+ }
162
+
163
+ // Summary
164
+ sections.push(generateSummary(cssResults, jsResults, inlineStyles, inlineScripts, missingImports, detectedLibraries, ghostData));
165
+
166
+ return sections.join('\n\n---\n\n');
167
+ }
168
+
169
+ /**
170
+ * Generate libraries section
171
+ */
172
+ function generateLibrariesSection(detectedLibraries, cssLibraryResults, jsLibraryResults, projectDir) {
173
+ let content = `## 📚 Libraries Detected\n\n`;
174
+ content += `> These libraries are used by this component. Make sure they are properly imported.\n\n`;
175
+
176
+ // Libraries from CDN
177
+ const cdnLibs = detectedLibraries.fromCDN || [];
178
+ if (cdnLibs.length > 0) {
179
+ content += `### Loaded via CDN ✅\n\n`;
180
+ content += `| Library | Type | URL |\n`;
181
+ content += `|---------|------|-----|\n`;
182
+ cdnLibs.forEach(lib => {
183
+ const shortUrl = lib.url.length > 50 ? lib.url.substring(0, 47) + '...' : lib.url;
184
+ content += `| **${lib.name}** | ${lib.type} | \`${shortUrl}\` |\n`;
185
+ });
186
+ content += '\n';
187
+ }
188
+
189
+ // Libraries from local files
190
+ const fileLibs = detectedLibraries.fromFiles || {};
191
+ const libNames = Object.keys(fileLibs);
192
+ if (libNames.length > 0) {
193
+ content += `### Local Library Files\n\n`;
194
+ content += `| Library | Type | Status | Website |\n`;
195
+ content += `|---------|------|--------|--------|\n`;
196
+
197
+ libNames.forEach(name => {
198
+ const lib = fileLibs[name];
199
+ const hasLinkedFiles = lib.files?.some(f => {
200
+ const allResults = [...cssLibraryResults, ...jsLibraryResults];
201
+ return allResults.find(r => r.filePath === f && r.isLinked);
202
+ });
203
+ const status = hasLinkedFiles ? '✅ Linked' : '⚠️ Not Linked';
204
+ const website = lib.website ? `[Docs](${lib.website})` : '-';
205
+ content += `| **${name}** | ${lib.type} | ${status} | ${website} |\n`;
206
+ });
207
+ content += '\n';
208
+ }
209
+
210
+ // Libraries detected from class names
211
+ const classLibs = detectedLibraries.fromClasses || [];
212
+ if (classLibs.length > 0) {
213
+ content += `### Detected from Class Names\n\n`;
214
+ content += `The following libraries appear to be used based on class naming conventions:\n\n`;
215
+ classLibs.forEach(name => {
216
+ content += `- **${name}**\n`;
217
+ });
218
+ content += '\n';
219
+ }
220
+
221
+ // Note about library code
222
+ content += `> **Note:** Library code is not shown in detail below to keep the report focused on your custom code.\n`;
223
+ content += `> The component uses ${cssLibraryResults.length} CSS rules and ${jsLibraryResults.length} JS references from libraries.`;
224
+
225
+ return content;
226
+ }
227
+
228
+ /**
229
+ * Generate header section
230
+ */
231
+ function generateHeader(targetInfo, htmlPath, generatedAt) {
232
+ const date = new Date(generatedAt).toLocaleString();
233
+
234
+ return `# Component Analysis: ${targetInfo.selector}
235
+
236
+ > Generated by **CodeScoop** on ${date}
237
+ >
238
+ > Source: \`${path.basename(htmlPath)}\`
239
+
240
+ ## Target Component
241
+
242
+ | Property | Value |
243
+ |----------|-------|
244
+ | **Selector** | \`${targetInfo.selector}\` |
245
+ | **Tag** | \`<${targetInfo.tagName}>\` |
246
+ | **Classes** | ${targetInfo.classes.length > 0 ? targetInfo.classes.map(c => '`.' + c + '`').join(', ') : '_none_'} |
247
+ | **IDs** | ${targetInfo.ids.length > 0 ? targetInfo.ids.map(id => '`#' + id + '`').join(', ') : '_none_'} |
248
+ | **Line Range** | ${targetInfo.startLine ? `Lines ${targetInfo.startLine}-${targetInfo.endLine}` : '_unknown_'} |`;
249
+ }
250
+
251
+ /**
252
+ * Generate missing imports warning
253
+ */
254
+ function generateMissingImportsWarning(missingImports, projectDir) {
255
+ const fileList = missingImports.map(f => `- \`${path.relative(projectDir, f)}\``).join('\n');
256
+
257
+ return `## ⚠️ Missing Imports Detected
258
+
259
+ The following files contain code relevant to this component but are **NOT imported** in the HTML file:
260
+
261
+ ${fileList}
262
+
263
+ > **This may cause the component to not work correctly!**
264
+ > Add the appropriate \`<link>\` or \`<script>\` tags to import these files.`;
265
+ }
266
+
267
+ /**
268
+ * Generate HTML section
269
+ */
270
+ function generateHTMLSection(targetInfo) {
271
+ return `## Target HTML
272
+ ${targetInfo.startLine ? `**Lines:** ${targetInfo.startLine}-${targetInfo.endLine}` : ''}
273
+
274
+ \`\`\`html
275
+ ${targetInfo.html}
276
+ \`\`\``;
277
+ }
278
+
279
+ /**
280
+ * Generate CSS section
281
+ */
282
+ function generateCSSSection(cssResults, inlineStyles, projectDir, title) {
283
+ let content = `## ${title}\n\n`;
284
+
285
+ // Inline styles first
286
+ if (inlineStyles.length > 0) {
287
+ content += `### Inline \`<style>\` Blocks\n\n`;
288
+
289
+ inlineStyles.forEach((style, index) => {
290
+ content += `#### Block ${index + 1}\n\n`;
291
+ content += '```css\n' + style.content + '\n```\n\n';
292
+ });
293
+ }
294
+
295
+ // External CSS files
296
+ cssResults.forEach(result => {
297
+ const relativePath = path.relative(projectDir, result.filePath);
298
+ const fileType = result.isScss ? 'SCSS' : (result.isMinified ? 'CSS (minified)' : 'CSS');
299
+
300
+ content += `### From: \`${relativePath}\`\n`;
301
+ content += `**Type:** ${fileType}\n\n`;
302
+
303
+ result.matches.forEach((match, index) => {
304
+ if (match.atRuleContext) {
305
+ content += `**Context:** \`${match.atRuleContext}\`\n`;
306
+ }
307
+
308
+ content += `**Lines:** ${match.startLine}-${match.endLine} | `;
309
+ content += `**Matched on:** ${match.matchedOn.join(', ')}\n\n`;
310
+
311
+ const lang = result.isScss ? 'scss' : 'css';
312
+ content += '```' + lang + '\n' + match.content + '\n```\n\n';
313
+ });
314
+ });
315
+
316
+ return content;
317
+ }
318
+
319
+ /**
320
+ * Generate JavaScript section
321
+ */
322
+ function generateJSSection(jsResults, inlineScripts, projectDir, title) {
323
+ let content = `## ${title}\n\n`;
324
+
325
+ // Inline scripts first
326
+ if (inlineScripts.length > 0) {
327
+ content += `### Inline \`<script>\` Blocks\n\n`;
328
+
329
+ inlineScripts.forEach((script, index) => {
330
+ content += `#### Block ${index + 1}\n\n`;
331
+ content += '```javascript\n' + script.content + '\n```\n\n';
332
+ });
333
+ }
334
+
335
+ // External JS files
336
+ jsResults.forEach(result => {
337
+ const relativePath = path.relative(projectDir, result.filePath);
338
+ const fileType = result.isMinified ? 'JS (minified, beautified for display)' : 'JavaScript';
339
+
340
+ content += `### From: \`${relativePath}\`\n`;
341
+ content += `**Type:** ${fileType}\n\n`;
342
+
343
+ result.matches.forEach((match, index) => {
344
+ content += `**Lines:** ${match.startLine}-${match.endLine} | `;
345
+ content += `**Type:** ${match.type} | `;
346
+ content += `**Matched on:** ${match.matchedOn.join(', ')}\n`;
347
+
348
+ if (match.selector) {
349
+ content += `**Selector:** \`${match.selector}\`\n`;
350
+ }
351
+
352
+ content += '\n```javascript\n' + match.content + '\n```\n\n';
353
+ });
354
+ });
355
+
356
+ return content;
357
+ }
358
+
359
+ /**
360
+ * Generate summary section
361
+ */
362
+ function generateSummary(cssResults, jsResults, inlineStyles, inlineScripts, missingImports, detectedLibraries = {}, ghostData = {}) {
363
+ const totalCSSRules = cssResults.reduce((sum, r) => sum + r.matches.length, 0) + inlineStyles.length;
364
+ const totalJSRefs = jsResults.reduce((sum, r) => sum + r.matches.length, 0) + inlineScripts.length;
365
+
366
+ const linkedCSSFiles = cssResults.filter(r => r.isLinked).length;
367
+ const linkedJSFiles = jsResults.filter(r => r.isLinked).length;
368
+ const unlinkedCSSFiles = cssResults.filter(r => !r.isLinked).length;
369
+ const unlinkedJSFiles = jsResults.filter(r => !r.isLinked).length;
370
+
371
+ const libraryCount = Object.keys(detectedLibraries.fromFiles || {}).length +
372
+ (detectedLibraries.fromCDN || []).length;
373
+
374
+ const ghostCount = ghostData.ghostClasses?.length || 0;
375
+
376
+ const advancedFeaturesSummary = updateSummaryForAdvancedFeatures(cssResults);
377
+
378
+ return `## Summary
379
+
380
+ | Metric | Count |
381
+ |--------|-------|
382
+ | **Libraries Detected** | ${libraryCount} |
383
+ | **Custom CSS Rules Found** | ${totalCSSRules} |
384
+ | **Custom JS References Found** | ${totalJSRefs} |
385
+ | **Linked CSS Files** | ${linkedCSSFiles} |
386
+ | **Linked JS Files** | ${linkedJSFiles} |
387
+ | **Unlinked CSS Files (potential issues)** | ${unlinkedCSSFiles} |
388
+ | **Unlinked JS Files (potential issues)** | ${unlinkedJSFiles} |
389
+ | **Inline Styles** | ${inlineStyles.length} |
390
+ | **Inline Scripts** | ${inlineScripts.length} |
391
+ ${ghostCount > 0 ? `| **👻 Ghost Classes (no CSS)** | ${ghostCount} |\n` : ''}${advancedFeaturesSummary}
392
+ ${missingImports.length > 0 ? `
393
+ ### ⚠️ Action Required
394
+ ${missingImports.length} file(s) contain relevant code but are not imported. Review the "Missing Imports" section above.
395
+ ` : '✅ All custom files with relevant code appear to be properly linked.'}
396
+
397
+ ---
398
+
399
+ *This report was generated by CodeScoop to help debug component dependencies. Feed this file to an LLM for assistance converting to React/Next.js or debugging issues.*`;
400
+ }
401
+
402
+ /**
403
+ * Generate summary-only section (no code blocks)
404
+ */
405
+ function generateSummaryOnlySection(cssResults, jsResults, projectDir) {
406
+ let content = `## 📋 Files Summary (Summary-Only Mode)\n\n`;
407
+ content += `> Code blocks omitted. Use without \`--summary-only\` flag to see full code.\n\n`;
408
+
409
+ // CSS Files
410
+ const cssWithMatches = cssResults.filter(r => r.matches && r.matches.length > 0);
411
+ if (cssWithMatches.length > 0) {
412
+ content += `### CSS Files (${cssWithMatches.length} files)\n\n`;
413
+ content += `| File | Matches | Linked |\n`;
414
+ content += `|------|---------|--------|\n`;
415
+ cssWithMatches.forEach(result => {
416
+ const relPath = path.relative(projectDir, result.filePath);
417
+ const count = result.originalCount || result.matches.length;
418
+ const linked = result.isLinked ? '✅' : '❌';
419
+ content += `| \`${relPath}\` | ${count} rules | ${linked} |\n`;
420
+ });
421
+ content += '\n';
422
+ }
423
+
424
+ // JS Files
425
+ const jsWithMatches = jsResults.filter(r => r.matches && r.matches.length > 0);
426
+ if (jsWithMatches.length > 0) {
427
+ content += `### JavaScript Files (${jsWithMatches.length} files)\n\n`;
428
+ content += `| File | Matches | Linked |\n`;
429
+ content += `|------|---------|--------|\n`;
430
+ jsWithMatches.forEach(result => {
431
+ const relPath = path.relative(projectDir, result.filePath);
432
+ const count = result.originalCount || result.matches.length;
433
+ const linked = result.isLinked ? '✅' : '❌';
434
+ content += `| \`${relPath}\` | ${count} refs | ${linked} |\n`;
435
+ });
436
+ content += '\n';
437
+ }
438
+
439
+ return content;
440
+ }
441
+
442
+ /**
443
+ * Generate Shadow DOM section (add to generateMarkdown after CSS section)
444
+ */
445
+ function generateShadowDOMSection(cssResults, projectDir) {
446
+ // Collect all Shadow DOM rules from all CSS results
447
+ const allShadowDOMRules = [];
448
+
449
+ for (const result of cssResults) {
450
+ if (result.shadowDOMRules && result.shadowDOMRules.length > 0) {
451
+ allShadowDOMRules.push({
452
+ file: result.filePath,
453
+ rules: result.shadowDOMRules
454
+ });
455
+ }
456
+ }
457
+
458
+ if (allShadowDOMRules.length === 0) {
459
+ return '';
460
+ }
461
+
462
+ let content = `## 🔮 Shadow DOM Styles\n\n`;
463
+ content += `> These styles target Web Component shadow DOM via \`::part()\` or \`::slotted()\`.\n`;
464
+ content += `> Shadow DOM provides style encapsulation for custom elements.\n\n`;
465
+
466
+ for (const { file, rules } of allShadowDOMRules) {
467
+ const relativePath = path.relative(projectDir, file);
468
+ content += `### From: \`${relativePath}\`\n\n`;
469
+
470
+ for (const rule of rules) {
471
+ if (rule.atRuleContext) {
472
+ content += `**Context:** \`${rule.atRuleContext}\`\n`;
473
+ }
474
+
475
+ content += `**Type:** \`${rule.shadowDOMType}\` | `;
476
+ content += `**Lines:** ${rule.startLine}-${rule.endLine} | `;
477
+ content += `**Matched on:** ${rule.matchedOn.join(', ')}\n\n`;
478
+
479
+ content += '```css\n' + rule.content + '\n```\n\n';
480
+ }
481
+ }
482
+
483
+ content += `> **Note:** Shadow DOM styles are isolated from global styles. `;
484
+ content += `\`::part()\` exposes specific shadow DOM elements for styling from outside.\n`;
485
+
486
+ return content;
487
+ }
488
+
489
+ /**
490
+ * Generate CSS Houdini section (add to generateMarkdown after variables section)
491
+ */
492
+ function generateHoudiniSection(cssResults, projectDir) {
493
+ // Collect all Houdini properties
494
+ const allHoudiniProps = [];
495
+
496
+ for (const result of cssResults) {
497
+ if (result.houdiniProperties && result.houdiniProperties.length > 0) {
498
+ allHoudiniProps.push({
499
+ file: result.filePath,
500
+ properties: result.houdiniProperties
501
+ });
502
+ }
503
+ }
504
+
505
+ if (allHoudiniProps.length === 0) {
506
+ return '';
507
+ }
508
+
509
+ let content = `## 🎨 CSS Houdini Custom Properties\n\n`;
510
+ content += `> CSS Houdini \`@property\` rules define custom properties with type checking and default values.\n`;
511
+ content += `> These provide more control than standard CSS variables.\n\n`;
512
+
513
+ for (const { file, properties } of allHoudiniProps) {
514
+ const relativePath = path.relative(projectDir, file);
515
+ content += `### From: \`${relativePath}\`\n\n`;
516
+
517
+ for (const prop of properties) {
518
+ if (prop.atRuleContext) {
519
+ content += `**Context:** \`${prop.atRuleContext}\`\n`;
520
+ }
521
+
522
+ content += `**Property:** \`${prop.propertyName}\` | `;
523
+ content += `**Lines:** ${prop.startLine}-${prop.endLine}\n\n`;
524
+
525
+ content += '```css\n' + prop.content + '\n```\n\n';
526
+ }
527
+ }
528
+
529
+ content += `> **Browser Support:** CSS Houdini has limited support. Check [caniuse.com](https://caniuse.com/css-properties-and-values-api) for compatibility.\n`;
530
+
531
+ return content;
532
+ }
533
+
534
+ /**
535
+ * Add to your summary section to track Shadow DOM and Houdini usage
536
+ */
537
+ function updateSummaryForAdvancedFeatures(cssResults) {
538
+ const shadowDOMCount = cssResults.reduce((sum, r) =>
539
+ sum + (r.shadowDOMRules?.length || 0), 0);
540
+
541
+ const houdiniCount = cssResults.reduce((sum, r) =>
542
+ sum + (r.houdiniProperties?.length || 0), 0);
543
+
544
+ let summaryAddition = '';
545
+
546
+ if (shadowDOMCount > 0) {
547
+ summaryAddition += `| **Shadow DOM Rules** | ${shadowDOMCount} |\n`;
548
+ }
549
+
550
+ if (houdiniCount > 0) {
551
+ summaryAddition += `| **Houdini @property Rules** | ${houdiniCount} |\n`;
552
+ }
553
+
554
+ return summaryAddition;
555
+ }
556
+
557
+ module.exports = {
558
+ generateMarkdown,
559
+ generateShadowDOMSection,
560
+ generateHoudiniSection,
561
+ updateSummaryForAdvancedFeatures
562
+ };