gbu-accessibility-package 3.5.0 โ†’ 3.8.1

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/lib/fixer.js CHANGED
@@ -2274,10 +2274,13 @@ class AccessibilityFixer {
2274
2274
  console.log(chalk.yellow('\n๐Ÿ“‘ Step 8: Heading analysis...'));
2275
2275
  results.headings = await this.analyzeHeadings(directory);
2276
2276
 
2277
- // Step 9: Check broken links (no auto-fix)
2278
- console.log(chalk.yellow('\n๐Ÿ”— Step 9: Broken links check...'));
2277
+ // Step 9: Check broken links and missing resources (no auto-fix)
2278
+ console.log(chalk.yellow('\n๐Ÿ”— Step 9: External links check...'));
2279
2279
  results.brokenLinks = await this.checkBrokenLinks(directory);
2280
2280
 
2281
+ console.log(chalk.yellow('\n๐Ÿ“ Step 9b: Missing resources check...'));
2282
+ results.missingResources = await this.check404Resources(directory);
2283
+
2281
2284
  // Step 10: Cleanup duplicate roles
2282
2285
  console.log(chalk.yellow('\n๐Ÿงน Step 10: Cleanup duplicate roles...'));
2283
2286
  results.cleanup = await this.cleanupDuplicateRoles(directory);
@@ -2866,9 +2869,9 @@ class AccessibilityFixer {
2866
2869
  return fixed;
2867
2870
  }
2868
2871
 
2869
- // Check for broken links and 404 resources
2872
+ // Check for broken external links only
2870
2873
  async checkBrokenLinks(directory = '.') {
2871
- console.log(chalk.blue('๐Ÿ”— Checking for broken links and 404 resources...'));
2874
+ console.log(chalk.blue('๐Ÿ”— Checking for broken external links...'));
2872
2875
 
2873
2876
  const htmlFiles = await this.findHtmlFiles(directory);
2874
2877
  const results = [];
@@ -2876,7 +2879,7 @@ class AccessibilityFixer {
2876
2879
  for (const file of htmlFiles) {
2877
2880
  try {
2878
2881
  const content = await fs.readFile(file, 'utf8');
2879
- const issues = await this.analyzeBrokenLinks(content, file);
2882
+ const issues = await this.analyzeBrokenLinks(content, file, 'external-only');
2880
2883
 
2881
2884
  if (issues.length > 0) {
2882
2885
  console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
@@ -2895,12 +2898,46 @@ class AccessibilityFixer {
2895
2898
  }
2896
2899
  }
2897
2900
 
2898
- console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed links in ${results.length} files`));
2901
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed external links in ${results.length} files`));
2899
2902
  console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
2900
2903
  return results;
2901
2904
  }
2902
2905
 
2903
- async analyzeBrokenLinks(content, filePath) {
2906
+ // Check for 404 resources (local files) only
2907
+ async check404Resources(directory = '.') {
2908
+ console.log(chalk.blue('๐Ÿ“ Checking for 404 resources (missing local files)...'));
2909
+
2910
+ const htmlFiles = await this.findHtmlFiles(directory);
2911
+ const results = [];
2912
+
2913
+ for (const file of htmlFiles) {
2914
+ try {
2915
+ const content = await fs.readFile(file, 'utf8');
2916
+ const issues = await this.analyzeBrokenLinks(content, file, 'local-only');
2917
+
2918
+ if (issues.length > 0) {
2919
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
2920
+ issues.forEach(issue => {
2921
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
2922
+ if (issue.suggestion) {
2923
+ console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
2924
+ }
2925
+ });
2926
+ }
2927
+
2928
+ results.push({ file, status: 'analyzed', issues: issues.length, missingResources: issues });
2929
+ } catch (error) {
2930
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
2931
+ results.push({ file, status: 'error', error: error.message });
2932
+ }
2933
+ }
2934
+
2935
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed local resources in ${results.length} files`));
2936
+ console.log(chalk.gray('๐Ÿ’ก Missing resource issues require manual review and cannot be auto-fixed'));
2937
+ return results;
2938
+ }
2939
+
2940
+ async analyzeBrokenLinks(content, filePath, mode = 'all') {
2904
2941
  const issues = [];
2905
2942
  const path = require('path');
2906
2943
  const http = require('http');
@@ -2908,20 +2945,33 @@ class AccessibilityFixer {
2908
2945
  const { URL } = require('url');
2909
2946
 
2910
2947
  // Extract all links and resources
2911
- const linkPatterns = [
2912
- // Anchor links
2913
- { pattern: /<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Link', element: 'a' },
2914
- // Images
2915
- { pattern: /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Image', element: 'img' },
2916
- // CSS links
2917
- { pattern: /<link[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'CSS', element: 'link' },
2918
- // Script sources
2919
- { pattern: /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Script', element: 'script' },
2920
- // Video sources
2921
- { pattern: /<video[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Video', element: 'video' },
2922
- // Audio sources
2923
- { pattern: /<audio[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Audio', element: 'audio' }
2924
- ];
2948
+ let linkPatterns = [];
2949
+
2950
+ if (mode === 'external-only') {
2951
+ // Only anchor links for external link checking
2952
+ linkPatterns = [
2953
+ { pattern: /<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Link', element: 'a' }
2954
+ ];
2955
+ } else if (mode === 'local-only') {
2956
+ // Only resources (not anchor links) for 404 resource checking
2957
+ linkPatterns = [
2958
+ { pattern: /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Image', element: 'img' },
2959
+ { pattern: /<link[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'CSS', element: 'link' },
2960
+ { pattern: /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Script', element: 'script' },
2961
+ { pattern: /<video[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Video', element: 'video' },
2962
+ { pattern: /<audio[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Audio', element: 'audio' }
2963
+ ];
2964
+ } else {
2965
+ // All patterns for comprehensive checking
2966
+ linkPatterns = [
2967
+ { pattern: /<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Link', element: 'a' },
2968
+ { pattern: /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Image', element: 'img' },
2969
+ { pattern: /<link[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'CSS', element: 'link' },
2970
+ { pattern: /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Script', element: 'script' },
2971
+ { pattern: /<video[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Video', element: 'video' },
2972
+ { pattern: /<audio[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Audio', element: 'audio' }
2973
+ ];
2974
+ }
2925
2975
 
2926
2976
  const baseDir = path.dirname(filePath);
2927
2977
 
@@ -2929,7 +2979,7 @@ class AccessibilityFixer {
2929
2979
  let match;
2930
2980
  while ((match = linkPattern.pattern.exec(content)) !== null) {
2931
2981
  const url = match[1];
2932
- const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element);
2982
+ const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element, mode);
2933
2983
  if (issue) {
2934
2984
  issues.push(issue);
2935
2985
  }
@@ -2939,7 +2989,7 @@ class AccessibilityFixer {
2939
2989
  return issues;
2940
2990
  }
2941
2991
 
2942
- async checkSingleLink(url, baseDir, resourceType, elementType) {
2992
+ async checkSingleLink(url, baseDir, resourceType, elementType, mode = 'all') {
2943
2993
  // Skip certain URLs
2944
2994
  if (this.shouldSkipUrl(url)) {
2945
2995
  return null;
@@ -2947,10 +2997,16 @@ class AccessibilityFixer {
2947
2997
 
2948
2998
  try {
2949
2999
  if (this.isExternalUrl(url)) {
2950
- // Check external URLs
3000
+ // Check external URLs only if mode allows
3001
+ if (mode === 'local-only') {
3002
+ return null; // Skip external URLs in local-only mode
3003
+ }
2951
3004
  return await this.checkExternalUrl(url, resourceType, elementType);
2952
3005
  } else {
2953
- // Check local files
3006
+ // Check local files only if mode allows
3007
+ if (mode === 'external-only') {
3008
+ return null; // Skip local files in external-only mode
3009
+ }
2954
3010
  return await this.checkLocalFile(url, baseDir, resourceType, elementType);
2955
3011
  }
2956
3012
  } catch (error) {
@@ -3050,13 +3106,14 @@ class AccessibilityFixer {
3050
3106
  async checkLocalFile(url, baseDir, resourceType, elementType) {
3051
3107
  const path = require('path');
3052
3108
 
3053
- // Handle relative URLs
3109
+ // Handle different URL types
3054
3110
  let filePath;
3055
3111
  if (url.startsWith('/')) {
3056
- // Absolute path from web root - we'll check relative to baseDir
3057
- filePath = path.join(baseDir, url.substring(1));
3112
+ // Absolute path from web root - find project root and resolve from there
3113
+ const projectRoot = this.findProjectRoot(baseDir);
3114
+ filePath = path.join(projectRoot, url.substring(1));
3058
3115
  } else {
3059
- // Relative path
3116
+ // Relative path - resolve relative to current HTML file directory
3060
3117
  filePath = path.resolve(baseDir, url);
3061
3118
  }
3062
3119
 
@@ -3070,11 +3127,41 @@ class AccessibilityFixer {
3070
3127
  suggestion: `Create the missing file or update the ${resourceType.toLowerCase()} path`,
3071
3128
  url: url,
3072
3129
  filePath: filePath,
3130
+ resolvedPath: filePath,
3073
3131
  resourceType: resourceType
3074
3132
  };
3075
3133
  }
3076
3134
  }
3077
3135
 
3136
+ // Helper method to find project root directory
3137
+ findProjectRoot(startDir) {
3138
+ const path = require('path');
3139
+ const fs = require('fs');
3140
+
3141
+ let currentDir = startDir;
3142
+ const root = path.parse(currentDir).root;
3143
+
3144
+ // Look for common project root indicators
3145
+ while (currentDir !== root) {
3146
+ // Check for package.json, .git, or other project indicators
3147
+ const packageJsonPath = path.join(currentDir, 'package.json');
3148
+ const gitPath = path.join(currentDir, '.git');
3149
+ const nodeModulesPath = path.join(currentDir, 'node_modules');
3150
+
3151
+ if (fs.existsSync(packageJsonPath) || fs.existsSync(gitPath) || fs.existsSync(nodeModulesPath)) {
3152
+ return currentDir;
3153
+ }
3154
+
3155
+ // Move up one directory
3156
+ const parentDir = path.dirname(currentDir);
3157
+ if (parentDir === currentDir) break; // Reached filesystem root
3158
+ currentDir = parentDir;
3159
+ }
3160
+
3161
+ // If no project root found, use the original baseDir (fallback)
3162
+ return startDir;
3163
+ }
3164
+
3078
3165
  // Analyze headings (no auto-fix, only suggestions)
3079
3166
  async analyzeHeadings(directory = '.') {
3080
3167
  console.log(chalk.blue('๐Ÿ“‘ Analyzing heading structure...'));
@@ -3612,12 +3699,18 @@ class AccessibilityFixer {
3612
3699
  results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
3613
3700
  console.log(chalk.gray('๐Ÿ’ก Heading issues require manual review and cannot be auto-fixed'));
3614
3701
 
3615
- // Step 9: Broken links check
3616
- console.log(chalk.blue('๐Ÿ”— Step 9: Broken links check...'));
3702
+ // Step 9: Broken links and missing resources check
3703
+ console.log(chalk.blue('๐Ÿ”— Step 9a: External links check...'));
3617
3704
  const brokenLinksResults = await this.checkBrokenLinks(directory);
3618
3705
  const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3619
- results.steps.push({ step: 9, name: 'Broken links check', issues: totalBrokenLinks });
3620
- console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
3706
+ results.steps.push({ step: '9a', name: 'External links check', issues: totalBrokenLinks });
3707
+
3708
+ console.log(chalk.blue('๐Ÿ“ Step 9b: Missing resources check...'));
3709
+ const missingResourcesResults = await this.check404Resources(directory);
3710
+ const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3711
+ results.steps.push({ step: '9b', name: 'Missing resources check', issues: totalMissingResources });
3712
+
3713
+ console.log(chalk.gray('๐Ÿ’ก Link and resource issues require manual review and cannot be auto-fixed'));
3621
3714
 
3622
3715
  // Step 10: Cleanup duplicate roles
3623
3716
  console.log(chalk.blue('๐Ÿงน Step 10: Cleanup duplicate roles...'));
@@ -3984,71 +4077,7 @@ class AccessibilityFixer {
3984
4077
  return results;
3985
4078
  }
3986
4079
 
3987
- async checkBrokenLinks(directory = '.') {
3988
- console.log(chalk.blue('๐Ÿ”— Checking for broken links and 404 resources...'));
3989
-
3990
- const htmlFiles = await this.findHtmlFiles(directory);
3991
- const results = [];
3992
- let totalIssuesFound = 0;
3993
-
3994
- for (const file of htmlFiles) {
3995
- try {
3996
- const content = await fs.readFile(file, 'utf8');
3997
- const issues = this.analyzeBrokenLinks(content, file);
3998
-
3999
- if (issues.length > 0) {
4000
- console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
4001
- issues.forEach(issue => {
4002
- console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4003
- if (issue.suggestion) {
4004
- console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
4005
- }
4006
- totalIssuesFound++;
4007
- });
4008
- }
4009
-
4010
- results.push({ file, status: 'analyzed', issues: issues.length });
4011
- } catch (error) {
4012
- console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
4013
- results.push({ file, status: 'error', error: error.message });
4014
- }
4015
- }
4016
-
4017
- console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed links in ${results.length} files`));
4018
- console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
4019
- return results;
4020
- }
4021
4080
 
4022
- analyzeBrokenLinks(content, filePath) {
4023
- const issues = [];
4024
-
4025
- // Check for local image files
4026
- const imgPattern = /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
4027
- let match;
4028
-
4029
- while ((match = imgPattern.exec(content)) !== null) {
4030
- const src = match[1];
4031
-
4032
- // Skip external URLs and data URLs
4033
- if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('//')) {
4034
- continue;
4035
- }
4036
-
4037
- // Check if local file exists
4038
- const fullPath = path.resolve(path.dirname(filePath), src);
4039
- try {
4040
- require('fs').statSync(fullPath);
4041
- } catch (error) {
4042
- issues.push({
4043
- type: '๐Ÿ“ Image not found',
4044
- description: `img file does not exist: ${src}`,
4045
- suggestion: 'Create the missing file or update the image path'
4046
- });
4047
- }
4048
- }
4049
-
4050
- return issues;
4051
- }
4052
4081
 
4053
4082
  async cleanupDuplicateRoles(directory = '.') {
4054
4083
  console.log(chalk.blue('๐Ÿงน Cleaning up duplicate role attributes...'));
@@ -4403,12 +4432,18 @@ class AccessibilityFixer {
4403
4432
  results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
4404
4433
  console.log(chalk.gray('๐Ÿ’ก Heading issues require manual review and cannot be auto-fixed'));
4405
4434
 
4406
- // Step 10: Broken links check
4407
- console.log(chalk.blue('๐Ÿ”— Step 10: Broken links check...'));
4435
+ // Step 10: Broken links and missing resources check
4436
+ console.log(chalk.blue('๐Ÿ”— Step 10a: External links check...'));
4408
4437
  const brokenLinksResults = await this.checkBrokenLinks(directory);
4409
4438
  const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4410
- results.steps.push({ step: 10, name: 'Broken links check', issues: totalBrokenLinks });
4411
- console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
4439
+ results.steps.push({ step: '10a', name: 'External links check', issues: totalBrokenLinks });
4440
+
4441
+ console.log(chalk.blue('๐Ÿ“ Step 10b: Missing resources check...'));
4442
+ const missingResourcesResults = await this.check404Resources(directory);
4443
+ const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4444
+ results.steps.push({ step: '10b', name: 'Missing resources check', issues: totalMissingResources });
4445
+
4446
+ console.log(chalk.gray('๐Ÿ’ก Link and resource issues require manual review and cannot be auto-fixed'));
4412
4447
 
4413
4448
  // Step 11: Cleanup duplicate roles
4414
4449
  console.log(chalk.blue('๐Ÿงน Step 11: Cleanup duplicate roles...'));
@@ -5088,37 +5123,1374 @@ class AccessibilityFixer {
5088
5123
  return fallbacks[lang] || fallbacks.en;
5089
5124
  }
5090
5125
 
5091
- async findHtmlFiles(directory) {
5092
- const files = [];
5126
+ /**
5127
+ * Fix heading structure issues
5128
+ * Ensures proper heading hierarchy: only one h1, proper nesting, no duplicates in same section
5129
+ */
5130
+ async fixHeadingStructure(directory = '.') {
5131
+ console.log(chalk.blue('๐Ÿ“‘ Fixing heading structure...'));
5093
5132
 
5094
- // Check if the path is a file or directory
5095
- const stat = await fs.stat(directory);
5133
+ const htmlFiles = await this.findHtmlFiles(directory);
5134
+ const results = [];
5135
+ let totalIssuesFound = 0;
5136
+ let totalFixesApplied = 0;
5096
5137
 
5097
- if (stat.isFile()) {
5098
- // If it's a file, check if it's HTML
5099
- if (directory.endsWith('.html')) {
5100
- files.push(directory);
5138
+ for (const file of htmlFiles) {
5139
+ try {
5140
+ const content = await fs.readFile(file, 'utf8');
5141
+ const analysis = this.analyzeHeadingStructure(content);
5142
+
5143
+ if (analysis.issues.length > 0) {
5144
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
5145
+ analysis.issues.forEach(issue => {
5146
+ console.log(chalk.yellow(` ๐Ÿ“‘ ${issue.type}: ${issue.description}`));
5147
+ console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
5148
+ totalIssuesFound++;
5149
+ });
5150
+ }
5151
+
5152
+ let fixed = content;
5153
+ let fixesApplied = 0;
5154
+
5155
+ if (this.config.autoFixHeadings) {
5156
+ const fixResult = this.fixHeadingStructureInContent(content, analysis);
5157
+ fixed = fixResult.content;
5158
+ fixesApplied = fixResult.fixes;
5159
+ totalFixesApplied += fixesApplied;
5160
+
5161
+ if (fixesApplied > 0) {
5162
+ console.log(chalk.green(`โœ… Fixed heading structure in: ${file} (${fixesApplied} fixes)`));
5163
+ }
5164
+ }
5165
+
5166
+ if (fixed !== content) {
5167
+ if (this.config.backupFiles) {
5168
+ await fs.writeFile(`${file}.backup`, content);
5169
+ }
5170
+
5171
+ if (!this.config.dryRun) {
5172
+ await fs.writeFile(file, fixed);
5173
+ }
5174
+
5175
+ results.push({
5176
+ file,
5177
+ status: 'fixed',
5178
+ issues: analysis.issues.length,
5179
+ fixes: fixesApplied
5180
+ });
5181
+ } else {
5182
+ results.push({
5183
+ file,
5184
+ status: 'no-change',
5185
+ issues: analysis.issues.length,
5186
+ fixes: 0
5187
+ });
5188
+ }
5189
+ } catch (error) {
5190
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
5191
+ results.push({ file, status: 'error', error: error.message });
5101
5192
  }
5102
- return files;
5103
5193
  }
5104
5194
 
5105
- // If it's a directory, scan recursively
5106
- async function scan(dir) {
5107
- const entries = await fs.readdir(dir, { withFileTypes: true });
5195
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} heading issues across ${results.length} files`));
5196
+ if (this.config.autoFixHeadings) {
5197
+ console.log(chalk.blue(` Fixed ${totalFixesApplied} heading issues automatically`));
5198
+ } else {
5199
+ console.log(chalk.gray('๐Ÿ’ก Use --auto-fix-headings option to enable automatic fixes'));
5200
+ }
5201
+
5202
+ return results;
5203
+ }
5204
+
5205
+ /**
5206
+ * Analyze heading structure for issues
5207
+ */
5208
+ analyzeHeadingStructure(content) {
5209
+ const issues = [];
5210
+ const headings = [];
5211
+
5212
+ // Extract all headings with their positions and context
5213
+ const headingRegex = /<h([1-6])([^>]*)>(.*?)<\/h[1-6]>/gi;
5214
+ let match;
5215
+ let headingIndex = 0;
5216
+
5217
+ while ((match = headingRegex.exec(content)) !== null) {
5218
+ headingIndex++;
5219
+ const level = parseInt(match[1]);
5220
+ const attributes = match[2];
5221
+ const text = match[3].replace(/<[^>]*>/g, '').trim();
5222
+ const fullMatch = match[0];
5223
+ const position = match.index;
5108
5224
 
5109
- for (const entry of entries) {
5110
- const fullPath = path.join(dir, entry.name);
5111
-
5112
- if (entry.isDirectory() && !entry.name.startsWith('.')) {
5113
- await scan(fullPath);
5114
- } else if (entry.isFile() && entry.name.endsWith('.html')) {
5115
- files.push(fullPath);
5225
+ // Find the section context (look for parent elements)
5226
+ const beforeContent = content.substring(0, position);
5227
+ const sectionContext = this.findSectionContext(beforeContent);
5228
+
5229
+ headings.push({
5230
+ level,
5231
+ text,
5232
+ attributes,
5233
+ fullMatch,
5234
+ position,
5235
+ index: headingIndex,
5236
+ sectionContext
5237
+ });
5238
+ }
5239
+
5240
+ if (headings.length === 0) {
5241
+ return { issues, headings };
5242
+ }
5243
+
5244
+ // Check for multiple h1 elements
5245
+ const h1Elements = headings.filter(h => h.level === 1);
5246
+ if (h1Elements.length > 1) {
5247
+ issues.push({
5248
+ type: 'Multiple h1 elements',
5249
+ description: `Found ${h1Elements.length} h1 elements, should have only one`,
5250
+ suggestion: 'Convert extra h1 elements to h2-h6 as appropriate',
5251
+ severity: 'error',
5252
+ headings: h1Elements,
5253
+ fix: 'convert-extra-h1'
5254
+ });
5255
+ } else if (h1Elements.length === 0) {
5256
+ issues.push({
5257
+ type: 'Missing h1 element',
5258
+ description: 'Page should have exactly one h1 element',
5259
+ suggestion: 'Convert the first heading to h1 or add an h1 element',
5260
+ severity: 'error',
5261
+ fix: 'add-missing-h1'
5262
+ });
5263
+ }
5264
+
5265
+ // Check for heading level skipping
5266
+ for (let i = 1; i < headings.length; i++) {
5267
+ const current = headings[i];
5268
+ const previous = headings[i - 1];
5269
+
5270
+ if (current.level > previous.level + 1) {
5271
+ issues.push({
5272
+ type: 'Heading level skip',
5273
+ description: `Heading level jumps from h${previous.level} to h${current.level}`,
5274
+ suggestion: `Use h${previous.level + 1} instead of h${current.level}`,
5275
+ severity: 'warning',
5276
+ heading: current,
5277
+ previousHeading: previous,
5278
+ fix: 'fix-level-skip'
5279
+ });
5280
+ }
5281
+ }
5282
+
5283
+ // Check for empty headings
5284
+ headings.forEach(heading => {
5285
+ if (!heading.text || heading.text.length === 0) {
5286
+ issues.push({
5287
+ type: 'Empty heading',
5288
+ description: `Heading ${heading.index} (h${heading.level}) is empty`,
5289
+ suggestion: 'Add descriptive text to the heading or remove it',
5290
+ severity: 'warning',
5291
+ heading,
5292
+ fix: 'fix-empty-heading'
5293
+ });
5294
+ }
5295
+ });
5296
+
5297
+ // Check for duplicate headings in the same section
5298
+ const sectionGroups = {};
5299
+ headings.forEach(heading => {
5300
+ const sectionKey = heading.sectionContext || 'root';
5301
+ if (!sectionGroups[sectionKey]) {
5302
+ sectionGroups[sectionKey] = [];
5303
+ }
5304
+ sectionGroups[sectionKey].push(heading);
5305
+ });
5306
+
5307
+ Object.entries(sectionGroups).forEach(([section, sectionHeadings]) => {
5308
+ const textGroups = {};
5309
+ sectionHeadings.forEach(heading => {
5310
+ if (heading.text) {
5311
+ const normalizedText = heading.text.toLowerCase().trim();
5312
+ if (!textGroups[normalizedText]) {
5313
+ textGroups[normalizedText] = [];
5314
+ }
5315
+ textGroups[normalizedText].push(heading);
5316
+ }
5317
+ });
5318
+
5319
+ Object.entries(textGroups).forEach(([text, duplicates]) => {
5320
+ if (duplicates.length > 1 && duplicates[0].level === duplicates[1].level) {
5321
+ issues.push({
5322
+ type: 'Duplicate heading',
5323
+ description: `Duplicate h${duplicates[0].level} heading: "${text}"`,
5324
+ suggestion: 'Make heading text unique or merge content',
5325
+ severity: 'warning',
5326
+ headings: duplicates,
5327
+ section,
5328
+ fix: 'fix-duplicate-heading'
5329
+ });
5330
+ }
5331
+ });
5332
+ });
5333
+
5334
+ return { issues, headings };
5335
+ }
5336
+
5337
+ /**
5338
+ * Find section context for a heading
5339
+ */
5340
+ findSectionContext(beforeContent) {
5341
+ // Look for common section elements
5342
+ const sectionPatterns = [
5343
+ /<section[^>]*>/gi,
5344
+ /<article[^>]*>/gi,
5345
+ /<main[^>]*>/gi,
5346
+ /<div[^>]*class[^>]*section[^>]*>/gi,
5347
+ /<div[^>]*id[^>]*section[^>]*>/gi
5348
+ ];
5349
+
5350
+ let lastSectionMatch = null;
5351
+ let lastSectionPosition = -1;
5352
+
5353
+ sectionPatterns.forEach(pattern => {
5354
+ let match;
5355
+ pattern.lastIndex = 0; // Reset regex
5356
+ while ((match = pattern.exec(beforeContent)) !== null) {
5357
+ if (match.index > lastSectionPosition) {
5358
+ lastSectionPosition = match.index;
5359
+ lastSectionMatch = match[0];
5116
5360
  }
5117
5361
  }
5362
+ });
5363
+
5364
+ return lastSectionMatch ? `section_${lastSectionPosition}` : 'root';
5365
+ }
5366
+
5367
+ /**
5368
+ * Fix heading structure issues in content
5369
+ */
5370
+ fixHeadingStructureInContent(content, analysis) {
5371
+ let fixed = content;
5372
+ let fixesApplied = 0;
5373
+
5374
+ if (!analysis || !analysis.issues) {
5375
+ return { content: fixed, fixes: fixesApplied };
5118
5376
  }
5119
5377
 
5120
- await scan(directory);
5121
- return files;
5378
+ // Sort issues by position (from end to start to avoid position shifts)
5379
+ const sortedIssues = analysis.issues
5380
+ .filter(issue => issue.fix)
5381
+ .sort((a, b) => {
5382
+ const posA = a.heading?.position || a.headings?.[0]?.position || 0;
5383
+ const posB = b.heading?.position || b.headings?.[0]?.position || 0;
5384
+ return posB - posA;
5385
+ });
5386
+
5387
+ sortedIssues.forEach(issue => {
5388
+ switch (issue.fix) {
5389
+ case 'convert-extra-h1':
5390
+ // Convert extra h1 elements to h2
5391
+ if (issue.headings && issue.headings.length > 1) {
5392
+ // Keep the first h1, convert others to h2
5393
+ for (let i = 1; i < issue.headings.length; i++) {
5394
+ const heading = issue.headings[i];
5395
+ const newHeading = heading.fullMatch.replace(/h1/gi, 'h2');
5396
+ fixed = fixed.replace(heading.fullMatch, newHeading);
5397
+ console.log(chalk.yellow(` ๐Ÿ“‘ Converted extra h1 to h2: "${heading.text}"`));
5398
+ fixesApplied++;
5399
+ }
5400
+ }
5401
+ break;
5402
+
5403
+ case 'add-missing-h1':
5404
+ // Convert the first heading to h1
5405
+ if (analysis.headings && analysis.headings.length > 0) {
5406
+ const firstHeading = analysis.headings[0];
5407
+ const newHeading = firstHeading.fullMatch.replace(
5408
+ new RegExp(`h${firstHeading.level}`, 'gi'),
5409
+ 'h1'
5410
+ );
5411
+ fixed = fixed.replace(firstHeading.fullMatch, newHeading);
5412
+ console.log(chalk.yellow(` ๐Ÿ“‘ Converted first heading to h1: "${firstHeading.text}"`));
5413
+ fixesApplied++;
5414
+ }
5415
+ break;
5416
+
5417
+ case 'fix-level-skip':
5418
+ // Fix heading level skipping
5419
+ if (issue.heading && issue.previousHeading) {
5420
+ const correctLevel = issue.previousHeading.level + 1;
5421
+ const newHeading = issue.heading.fullMatch.replace(
5422
+ new RegExp(`h${issue.heading.level}`, 'gi'),
5423
+ `h${correctLevel}`
5424
+ );
5425
+ fixed = fixed.replace(issue.heading.fullMatch, newHeading);
5426
+ console.log(chalk.yellow(` ๐Ÿ“‘ Fixed level skip: h${issue.heading.level} โ†’ h${correctLevel} for "${issue.heading.text}"`));
5427
+ fixesApplied++;
5428
+ }
5429
+ break;
5430
+
5431
+ case 'fix-empty-heading':
5432
+ // Add text to empty headings based on context
5433
+ if (issue.heading) {
5434
+ const contextText = this.generateHeadingText(issue.heading, content);
5435
+ const newHeading = issue.heading.fullMatch.replace(
5436
+ /(<h[1-6][^>]*>)(\s*)(.*?)(\s*)(<\/h[1-6]>)/i,
5437
+ `$1${contextText}$5`
5438
+ );
5439
+ fixed = fixed.replace(issue.heading.fullMatch, newHeading);
5440
+ console.log(chalk.yellow(` ๐Ÿ“‘ Added text to empty heading: "${contextText}"`));
5441
+ fixesApplied++;
5442
+ }
5443
+ break;
5444
+
5445
+ case 'fix-duplicate-heading':
5446
+ // Make duplicate headings unique by adding numbers
5447
+ if (issue.headings && issue.headings.length > 1) {
5448
+ for (let i = 1; i < issue.headings.length; i++) {
5449
+ const heading = issue.headings[i];
5450
+ const newText = `${heading.text} (${i + 1})`;
5451
+ const newHeading = heading.fullMatch.replace(
5452
+ heading.text,
5453
+ newText
5454
+ );
5455
+ fixed = fixed.replace(heading.fullMatch, newHeading);
5456
+ console.log(chalk.yellow(` ๐Ÿ“‘ Made duplicate heading unique: "${newText}"`));
5457
+ fixesApplied++;
5458
+ }
5459
+ }
5460
+ break;
5461
+ }
5462
+ });
5463
+
5464
+ return { content: fixed, fixes: fixesApplied };
5465
+ }
5466
+
5467
+ /**
5468
+ * Generate appropriate text for empty headings based on context
5469
+ */
5470
+ generateHeadingText(heading, content) {
5471
+ // Look for context around the heading
5472
+ const position = heading.position;
5473
+ const beforeText = content.substring(Math.max(0, position - 200), position);
5474
+ const afterText = content.substring(position + heading.fullMatch.length, position + heading.fullMatch.length + 200);
5475
+
5476
+ // Extract meaningful words from surrounding content
5477
+ const contextWords = (beforeText + ' ' + afterText)
5478
+ .replace(/<[^>]*>/g, ' ')
5479
+ .replace(/[^\w\s]/g, ' ')
5480
+ .split(/\s+/)
5481
+ .filter(word => word.length > 3 && !/^\d+$/.test(word))
5482
+ .slice(0, 3);
5483
+
5484
+ if (contextWords.length > 0) {
5485
+ return contextWords.join(' ');
5486
+ }
5487
+
5488
+ // Fallback based on heading level
5489
+ const levelNames = {
5490
+ 1: 'Main Content',
5491
+ 2: 'Section',
5492
+ 3: 'Subsection',
5493
+ 4: 'Details',
5494
+ 5: 'Information',
5495
+ 6: 'Notes'
5496
+ };
5497
+
5498
+ return levelNames[heading.level] || 'Content';
5499
+ }
5500
+
5501
+ async findHtmlFiles(directory) {
5502
+ const files = [];
5503
+
5504
+ // Check if the path is a file or directory
5505
+ const stat = await fs.stat(directory);
5506
+
5507
+ if (stat.isFile()) {
5508
+ // If it's a file, check if it's HTML
5509
+ if (directory.endsWith('.html')) {
5510
+ files.push(directory);
5511
+ }
5512
+ return files;
5513
+ }
5514
+
5515
+ // If it's a directory, scan recursively
5516
+ async function scan(dir) {
5517
+ const entries = await fs.readdir(dir, { withFileTypes: true });
5518
+
5519
+ for (const entry of entries) {
5520
+ const fullPath = path.join(dir, entry.name);
5521
+
5522
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
5523
+ await scan(fullPath);
5524
+ } else if (entry.isFile() && entry.name.endsWith('.html')) {
5525
+ files.push(fullPath);
5526
+ }
5527
+ }
5528
+ }
5529
+
5530
+ await scan(directory);
5531
+ return files;
5532
+ }
5533
+
5534
+ // Check for unused files in the project
5535
+ async checkUnusedFiles(directory = '.') {
5536
+ console.log(chalk.blue('๐Ÿ—‚๏ธ Checking for unused files...'));
5537
+
5538
+ const results = [];
5539
+ const allFiles = await this.findAllProjectFiles(directory);
5540
+ const referencedFiles = await this.findReferencedFiles(directory);
5541
+
5542
+ // Normalize paths for comparison
5543
+ const normalizedReferenced = new Set();
5544
+ referencedFiles.forEach(file => {
5545
+ normalizedReferenced.add(path.resolve(file));
5546
+ });
5547
+
5548
+ for (const file of allFiles) {
5549
+ const absolutePath = path.resolve(file);
5550
+
5551
+ // Skip certain files that are typically not referenced directly
5552
+ if (this.shouldSkipUnusedCheck(file)) {
5553
+ continue;
5554
+ }
5555
+
5556
+ if (!normalizedReferenced.has(absolutePath)) {
5557
+ const relativePath = path.relative(directory, file);
5558
+ const fileType = this.getFileType(file);
5559
+
5560
+ console.log(chalk.yellow(` ๐Ÿ—‘๏ธ Unused ${fileType}: ${relativePath}`));
5561
+
5562
+ results.push({
5563
+ type: `๐Ÿ—‘๏ธ Unused ${fileType}`,
5564
+ description: `File not referenced anywhere: ${relativePath}`,
5565
+ suggestion: `Consider removing if truly unused: ${relativePath}`,
5566
+ filePath: file,
5567
+ fileType: fileType,
5568
+ relativePath: relativePath
5569
+ });
5570
+ }
5571
+ }
5572
+
5573
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${results.length} potentially unused files`));
5574
+ console.log(chalk.gray('๐Ÿ’ก Review carefully before removing - some files may be referenced dynamically'));
5575
+
5576
+ return results;
5577
+ }
5578
+
5579
+ async findAllProjectFiles(directory) {
5580
+ const files = [];
5581
+ const extensions = ['.html', '.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.pdf', '.mp4', '.webm', '.mp3', '.wav'];
5582
+
5583
+ async function scan(dir) {
5584
+ try {
5585
+ const entries = await fs.readdir(dir, { withFileTypes: true });
5586
+
5587
+ for (const entry of entries) {
5588
+ const fullPath = path.join(dir, entry.name);
5589
+
5590
+ if (entry.isDirectory()) {
5591
+ // Skip common directories that shouldn't be analyzed
5592
+ if (!this.shouldSkipDirectory(entry.name)) {
5593
+ await scan(fullPath);
5594
+ }
5595
+ } else if (entry.isFile()) {
5596
+ const ext = path.extname(entry.name).toLowerCase();
5597
+ if (extensions.includes(ext)) {
5598
+ files.push(fullPath);
5599
+ }
5600
+ }
5601
+ }
5602
+ } catch (error) {
5603
+ // Skip directories we can't read
5604
+ }
5605
+ }
5606
+
5607
+ await scan.call(this, directory);
5608
+ return files;
5609
+ }
5610
+
5611
+ async findReferencedFiles(directory) {
5612
+ const referenced = new Set();
5613
+ const htmlFiles = await this.findHtmlFiles(directory);
5614
+ const cssFiles = await this.findCssFiles(directory);
5615
+ const jsFiles = await this.findJsFiles(directory);
5616
+
5617
+ // Find references in HTML files
5618
+ for (const htmlFile of htmlFiles) {
5619
+ try {
5620
+ const content = await fs.readFile(htmlFile, 'utf8');
5621
+ const refs = this.extractFileReferences(content, path.dirname(htmlFile));
5622
+ refs.forEach(ref => referenced.add(ref));
5623
+ } catch (error) {
5624
+ // Skip files we can't read
5625
+ }
5626
+ }
5627
+
5628
+ // Find references in CSS files
5629
+ for (const cssFile of cssFiles) {
5630
+ try {
5631
+ const content = await fs.readFile(cssFile, 'utf8');
5632
+ const refs = this.extractCssReferences(content, path.dirname(cssFile));
5633
+ refs.forEach(ref => referenced.add(ref));
5634
+ } catch (error) {
5635
+ // Skip files we can't read
5636
+ }
5637
+ }
5638
+
5639
+ // Find references in JS files
5640
+ for (const jsFile of jsFiles) {
5641
+ try {
5642
+ const content = await fs.readFile(jsFile, 'utf8');
5643
+ const refs = this.extractJsReferences(content, path.dirname(jsFile));
5644
+ refs.forEach(ref => referenced.add(ref));
5645
+ } catch (error) {
5646
+ // Skip files we can't read
5647
+ }
5648
+ }
5649
+
5650
+ return Array.from(referenced);
5651
+ }
5652
+
5653
+ extractFileReferences(content, baseDir) {
5654
+ const references = [];
5655
+
5656
+ // HTML patterns for file references
5657
+ const patterns = [
5658
+ // Images
5659
+ /<img[^>]*src\s*=\s*["']([^"']+)["']/gi,
5660
+ // Links (CSS, other files)
5661
+ /<link[^>]*href\s*=\s*["']([^"']+)["']/gi,
5662
+ // Scripts
5663
+ /<script[^>]*src\s*=\s*["']([^"']+)["']/gi,
5664
+ // Anchors
5665
+ /<a[^>]*href\s*=\s*["']([^"']+)["']/gi,
5666
+ // Video/Audio
5667
+ /<(?:video|audio)[^>]*src\s*=\s*["']([^"']+)["']/gi,
5668
+ // Object/Embed
5669
+ /<(?:object|embed)[^>]*src\s*=\s*["']([^"']+)["']/gi,
5670
+ // Iframe
5671
+ /<iframe[^>]*src\s*=\s*["']([^"']+)["']/gi,
5672
+ // Meta (for icons)
5673
+ /<meta[^>]*content\s*=\s*["']([^"']+\.(ico|png|jpg|jpeg|svg))["']/gi
5674
+ ];
5675
+
5676
+ for (const pattern of patterns) {
5677
+ let match;
5678
+ while ((match = pattern.exec(content)) !== null) {
5679
+ const url = match[1];
5680
+ if (this.isLocalFile(url)) {
5681
+ const resolvedPath = this.resolveFilePath(url, baseDir);
5682
+ if (resolvedPath) {
5683
+ references.push(resolvedPath);
5684
+ }
5685
+ }
5686
+ }
5687
+ }
5688
+
5689
+ return references;
5690
+ }
5691
+
5692
+ extractCssReferences(content, baseDir) {
5693
+ const references = [];
5694
+
5695
+ // CSS patterns for file references
5696
+ const patterns = [
5697
+ // url() function
5698
+ /url\s*\(\s*["']?([^"')]+)["']?\s*\)/gi,
5699
+ // @import
5700
+ /@import\s+["']([^"']+)["']/gi
5701
+ ];
5702
+
5703
+ for (const pattern of patterns) {
5704
+ let match;
5705
+ while ((match = pattern.exec(content)) !== null) {
5706
+ const url = match[1];
5707
+ if (this.isLocalFile(url)) {
5708
+ const resolvedPath = this.resolveFilePath(url, baseDir);
5709
+ if (resolvedPath) {
5710
+ references.push(resolvedPath);
5711
+ }
5712
+ }
5713
+ }
5714
+ }
5715
+
5716
+ return references;
5717
+ }
5718
+
5719
+ extractJsReferences(content, baseDir) {
5720
+ const references = [];
5721
+
5722
+ // JavaScript patterns for file references
5723
+ const patterns = [
5724
+ // require() calls
5725
+ /require\s*\(\s*["']([^"']+)["']\s*\)/gi,
5726
+ // import statements
5727
+ /import\s+.*?from\s+["']([^"']+)["']/gi,
5728
+ // fetch() calls with local files
5729
+ /fetch\s*\(\s*["']([^"']*\.(html|css|js|json|xml))["']\s*\)/gi,
5730
+ // XMLHttpRequest
5731
+ /\.open\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']*\.(html|css|js|json|xml))["']/gi
5732
+ ];
5733
+
5734
+ for (const pattern of patterns) {
5735
+ let match;
5736
+ while ((match = pattern.exec(content)) !== null) {
5737
+ const url = match[1];
5738
+ if (this.isLocalFile(url)) {
5739
+ const resolvedPath = this.resolveFilePath(url, baseDir);
5740
+ if (resolvedPath) {
5741
+ references.push(resolvedPath);
5742
+ }
5743
+ }
5744
+ }
5745
+ }
5746
+
5747
+ return references;
5748
+ }
5749
+
5750
+ async findCssFiles(directory) {
5751
+ const files = [];
5752
+
5753
+ async function scan(dir) {
5754
+ try {
5755
+ const entries = await fs.readdir(dir, { withFileTypes: true });
5756
+
5757
+ for (const entry of entries) {
5758
+ const fullPath = path.join(dir, entry.name);
5759
+
5760
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
5761
+ await scan(fullPath);
5762
+ } else if (entry.isFile() && entry.name.endsWith('.css')) {
5763
+ files.push(fullPath);
5764
+ }
5765
+ }
5766
+ } catch (error) {
5767
+ // Skip directories we can't read
5768
+ }
5769
+ }
5770
+
5771
+ await scan(directory);
5772
+ return files;
5773
+ }
5774
+
5775
+ async findJsFiles(directory) {
5776
+ const files = [];
5777
+
5778
+ async function scan(dir) {
5779
+ try {
5780
+ const entries = await fs.readdir(dir, { withFileTypes: true });
5781
+
5782
+ for (const entry of entries) {
5783
+ const fullPath = path.join(dir, entry.name);
5784
+
5785
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
5786
+ await scan(fullPath);
5787
+ } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.mjs'))) {
5788
+ files.push(fullPath);
5789
+ }
5790
+ }
5791
+ } catch (error) {
5792
+ // Skip directories we can't read
5793
+ }
5794
+ }
5795
+
5796
+ await scan(directory);
5797
+ return files;
5798
+ }
5799
+
5800
+ shouldSkipDirectory(dirName) {
5801
+ const skipDirs = [
5802
+ 'node_modules', '.git', '.svn', '.hg', 'bower_components',
5803
+ 'vendor', 'tmp', 'temp', '.cache', 'dist', 'build', 'coverage',
5804
+ '.nyc_output', '.vscode', '.idea', '__pycache__', '.DS_Store'
5805
+ ];
5806
+ return skipDirs.includes(dirName) || dirName.startsWith('.');
5807
+ }
5808
+
5809
+ shouldSkipUnusedCheck(filePath) {
5810
+ const fileName = path.basename(filePath);
5811
+ const dirName = path.basename(path.dirname(filePath));
5812
+
5813
+ // Skip certain file types and patterns
5814
+ const skipPatterns = [
5815
+ // Common files that might not be directly referenced
5816
+ /^(index|main|app)\.(html|js|css)$/i,
5817
+ /^(readme|license|changelog)/i,
5818
+ /^package\.json$/i,
5819
+ /^\.gitignore$/i,
5820
+ /^favicon\.(ico|png)$/i,
5821
+ /^robots\.txt$/i,
5822
+ /^sitemap\.xml$/i,
5823
+ // Backup files
5824
+ /\.backup$/i,
5825
+ // Test directories
5826
+ /test|spec|__tests__/i
5827
+ ];
5828
+
5829
+ return skipPatterns.some(pattern =>
5830
+ pattern.test(fileName) || pattern.test(dirName)
5831
+ );
5832
+ }
5833
+
5834
+ getFileType(filePath) {
5835
+ const ext = path.extname(filePath).toLowerCase();
5836
+ const typeMap = {
5837
+ '.html': 'HTML',
5838
+ '.css': 'CSS',
5839
+ '.js': 'JavaScript',
5840
+ '.mjs': 'JavaScript Module',
5841
+ '.jpg': 'Image',
5842
+ '.jpeg': 'Image',
5843
+ '.png': 'Image',
5844
+ '.gif': 'Image',
5845
+ '.svg': 'SVG',
5846
+ '.webp': 'Image',
5847
+ '.ico': 'Icon',
5848
+ '.pdf': 'PDF',
5849
+ '.mp4': 'Video',
5850
+ '.webm': 'Video',
5851
+ '.mp3': 'Audio',
5852
+ '.wav': 'Audio'
5853
+ };
5854
+
5855
+ return typeMap[ext] || 'File';
5856
+ }
5857
+
5858
+ isLocalFile(url) {
5859
+ return !url.startsWith('http://') &&
5860
+ !url.startsWith('https://') &&
5861
+ !url.startsWith('//') &&
5862
+ !url.startsWith('data:') &&
5863
+ !url.startsWith('mailto:') &&
5864
+ !url.startsWith('tel:') &&
5865
+ !url.startsWith('#');
5866
+ }
5867
+
5868
+ resolveFilePath(url, baseDir) {
5869
+ try {
5870
+ let filePath;
5871
+
5872
+ if (url.startsWith('/')) {
5873
+ // Absolute path from web root
5874
+ filePath = path.join(baseDir, url.substring(1));
5875
+ } else {
5876
+ // Relative path
5877
+ filePath = path.resolve(baseDir, url);
5878
+ }
5879
+
5880
+ return filePath;
5881
+ } catch (error) {
5882
+ return null;
5883
+ }
5884
+ }
5885
+
5886
+ // Check for dead code in CSS and JavaScript files
5887
+ async checkDeadCode(directory = '.') {
5888
+ console.log(chalk.blue('โ˜ ๏ธ Checking for dead code...'));
5889
+
5890
+ const results = [];
5891
+
5892
+ // Check CSS dead code
5893
+ const cssDeadCode = await this.checkDeadCss(directory);
5894
+ results.push(...cssDeadCode);
5895
+
5896
+ // Check JavaScript dead code
5897
+ const jsDeadCode = await this.checkDeadJs(directory);
5898
+ results.push(...jsDeadCode);
5899
+
5900
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${results.length} potential dead code issues`));
5901
+ console.log(chalk.gray('๐Ÿ’ก Dead code analysis is heuristic - manual review recommended'));
5902
+
5903
+ return results;
5904
+ }
5905
+
5906
+ async checkDeadCss(directory) {
5907
+ console.log(chalk.cyan('๐ŸŽจ Analyzing CSS dead code...'));
5908
+
5909
+ const results = [];
5910
+ const cssFiles = await this.findCssFiles(directory);
5911
+ const htmlFiles = await this.findHtmlFiles(directory);
5912
+
5913
+ // Get all HTML content to check against
5914
+ let allHtmlContent = '';
5915
+ for (const htmlFile of htmlFiles) {
5916
+ try {
5917
+ const content = await fs.readFile(htmlFile, 'utf8');
5918
+ allHtmlContent += content + '\n';
5919
+ } catch (error) {
5920
+ // Skip files we can't read
5921
+ }
5922
+ }
5923
+
5924
+ for (const cssFile of cssFiles) {
5925
+ try {
5926
+ const content = await fs.readFile(cssFile, 'utf8');
5927
+ const deadRules = this.findDeadCssRules(content, allHtmlContent);
5928
+
5929
+ if (deadRules.length > 0) {
5930
+ const relativePath = path.relative(directory, cssFile);
5931
+ console.log(chalk.cyan(`\n๐Ÿ“ ${relativePath}:`));
5932
+
5933
+ deadRules.forEach(rule => {
5934
+ console.log(chalk.yellow(` โ˜ ๏ธ Potentially dead CSS: ${rule.selector}`));
5935
+ results.push({
5936
+ type: 'โ˜ ๏ธ Dead CSS rule',
5937
+ description: `CSS selector not found in HTML: ${rule.selector}`,
5938
+ suggestion: `Consider removing unused CSS rule: ${rule.selector}`,
5939
+ filePath: cssFile,
5940
+ relativePath: relativePath,
5941
+ selector: rule.selector,
5942
+ lineNumber: rule.lineNumber
5943
+ });
5944
+ });
5945
+ }
5946
+
5947
+ } catch (error) {
5948
+ console.error(chalk.red(`โŒ Error analyzing CSS ${cssFile}: ${error.message}`));
5949
+ }
5950
+ }
5951
+
5952
+ return results;
5953
+ }
5954
+
5955
+ findDeadCssRules(cssContent, htmlContent) {
5956
+ const deadRules = [];
5957
+
5958
+ // Simple CSS parser to extract selectors
5959
+ const cssRules = this.parseCssSelectors(cssContent);
5960
+
5961
+ for (const rule of cssRules) {
5962
+ if (this.isCssSelectorUsed(rule.selector, htmlContent)) {
5963
+ continue; // Selector is used
5964
+ }
5965
+
5966
+ // Skip certain selectors that are commonly dynamic
5967
+ if (this.shouldSkipCssSelector(rule.selector)) {
5968
+ continue;
5969
+ }
5970
+
5971
+ deadRules.push(rule);
5972
+ }
5973
+
5974
+ return deadRules;
5975
+ }
5976
+
5977
+ parseCssSelectors(cssContent) {
5978
+ const rules = [];
5979
+ const lines = cssContent.split('\n');
5980
+ let inRule = false;
5981
+
5982
+ for (let i = 0; i < lines.length; i++) {
5983
+ const line = lines[i].trim();
5984
+
5985
+ // Skip comments and empty lines
5986
+ if (line.startsWith('/*') || line.includes('*/') || !line) {
5987
+ continue;
5988
+ }
5989
+
5990
+ // Skip @rules like @media, @keyframes
5991
+ if (line.startsWith('@')) {
5992
+ continue;
5993
+ }
5994
+
5995
+ // Check if we're entering a rule block
5996
+ if (line.includes('{')) {
5997
+ inRule = true;
5998
+ // Extract selector part before the {
5999
+ const selectorPart = line.split('{')[0].trim();
6000
+ if (selectorPart && !selectorPart.includes(':') && !selectorPart.includes(';')) {
6001
+ rules.push({
6002
+ selector: selectorPart,
6003
+ lineNumber: i + 1
6004
+ });
6005
+ }
6006
+ continue;
6007
+ }
6008
+
6009
+ // Check if we're exiting a rule block
6010
+ if (line.includes('}')) {
6011
+ inRule = false;
6012
+ continue;
6013
+ }
6014
+
6015
+ // If we're not in a rule and line doesn't contain CSS properties, it might be a selector
6016
+ if (!inRule && !line.includes(':') && !line.includes(';') && line.length > 0) {
6017
+ rules.push({
6018
+ selector: line,
6019
+ lineNumber: i + 1
6020
+ });
6021
+ }
6022
+ }
6023
+
6024
+ return rules;
6025
+ }
6026
+
6027
+ isCssSelectorUsed(selector, htmlContent) {
6028
+ // Simple heuristic checks
6029
+
6030
+ // Check for class selectors
6031
+ if (selector.startsWith('.')) {
6032
+ const className = selector.substring(1).split(/[:\s>+~]/)[0];
6033
+ return htmlContent.includes(`class="${className}"`) ||
6034
+ htmlContent.includes(`class='${className}'`) ||
6035
+ htmlContent.includes(`class=".*${className}.*"`) ||
6036
+ htmlContent.includes(`class='.*${className}.*'`);
6037
+ }
6038
+
6039
+ // Check for ID selectors
6040
+ if (selector.startsWith('#')) {
6041
+ const idName = selector.substring(1).split(/[:\s>+~]/)[0];
6042
+ return htmlContent.includes(`id="${idName}"`) ||
6043
+ htmlContent.includes(`id='${idName}'`);
6044
+ }
6045
+
6046
+ // Check for tag selectors
6047
+ const tagMatch = selector.match(/^([a-zA-Z]+)/);
6048
+ if (tagMatch) {
6049
+ const tagName = tagMatch[1].toLowerCase();
6050
+ return htmlContent.toLowerCase().includes(`<${tagName}`);
6051
+ }
6052
+
6053
+ return true; // Conservative approach - assume complex selectors are used
6054
+ }
6055
+
6056
+ shouldSkipCssSelector(selector) {
6057
+ const skipPatterns = [
6058
+ // Pseudo-classes and pseudo-elements
6059
+ /:hover|:focus|:active|:visited|:before|:after/,
6060
+ // Media queries and complex selectors
6061
+ /@media|@keyframes|@font-face/,
6062
+ // Dynamic classes that might be added by JavaScript
6063
+ /\.active|\.selected|\.hidden|\.show|\.hide/,
6064
+ // Common framework classes
6065
+ /\.btn|\.form|\.nav|\.modal|\.tooltip/
6066
+ ];
6067
+
6068
+ return skipPatterns.some(pattern => pattern.test(selector));
6069
+ }
6070
+
6071
+ async checkDeadJs(directory) {
6072
+ console.log(chalk.cyan('๐Ÿ“œ Analyzing JavaScript dead code...'));
6073
+
6074
+ const results = [];
6075
+ const jsFiles = await this.findJsFiles(directory);
6076
+ const htmlFiles = await this.findHtmlFiles(directory);
6077
+
6078
+ // Get all HTML content to check against
6079
+ let allHtmlContent = '';
6080
+ for (const htmlFile of htmlFiles) {
6081
+ try {
6082
+ const content = await fs.readFile(htmlFile, 'utf8');
6083
+ allHtmlContent += content + '\n';
6084
+ } catch (error) {
6085
+ // Skip files we can't read
6086
+ }
6087
+ }
6088
+
6089
+ for (const jsFile of jsFiles) {
6090
+ try {
6091
+ const content = await fs.readFile(jsFile, 'utf8');
6092
+ const deadCode = this.findDeadJsCode(content, allHtmlContent, jsFile);
6093
+
6094
+ if (deadCode.length > 0) {
6095
+ const relativePath = path.relative(directory, jsFile);
6096
+ console.log(chalk.cyan(`\n๐Ÿ“ ${relativePath}:`));
6097
+
6098
+ deadCode.forEach(code => {
6099
+ console.log(chalk.yellow(` โ˜ ๏ธ Potentially dead JS: ${code.name}`));
6100
+ results.push({
6101
+ type: 'โ˜ ๏ธ Dead JavaScript',
6102
+ description: `${code.type} not referenced: ${code.name}`,
6103
+ suggestion: `Consider removing unused ${code.type.toLowerCase()}: ${code.name}`,
6104
+ filePath: jsFile,
6105
+ relativePath: relativePath,
6106
+ name: code.name,
6107
+ codeType: code.type,
6108
+ lineNumber: code.lineNumber
6109
+ });
6110
+ });
6111
+ }
6112
+
6113
+ } catch (error) {
6114
+ console.error(chalk.red(`โŒ Error analyzing JS ${jsFile}: ${error.message}`));
6115
+ }
6116
+ }
6117
+
6118
+ return results;
6119
+ }
6120
+
6121
+ findDeadJsCode(jsContent, htmlContent, jsFilePath) {
6122
+ const deadCode = [];
6123
+
6124
+ // Find function declarations
6125
+ const functions = this.parseJsFunctions(jsContent);
6126
+ const variables = this.parseJsVariables(jsContent);
6127
+
6128
+ // Check if functions are used
6129
+ for (const func of functions) {
6130
+ if (!this.isJsFunctionUsed(func.name, jsContent, htmlContent)) {
6131
+ deadCode.push({
6132
+ type: 'Function',
6133
+ name: func.name,
6134
+ lineNumber: func.lineNumber
6135
+ });
6136
+ }
6137
+ }
6138
+
6139
+ // Check if variables are used
6140
+ for (const variable of variables) {
6141
+ if (!this.isJsVariableUsed(variable.name, jsContent, htmlContent)) {
6142
+ deadCode.push({
6143
+ type: 'Variable',
6144
+ name: variable.name,
6145
+ lineNumber: variable.lineNumber
6146
+ });
6147
+ }
6148
+ }
6149
+
6150
+ return deadCode;
6151
+ }
6152
+
6153
+ parseJsFunctions(jsContent) {
6154
+ const functions = [];
6155
+ const lines = jsContent.split('\n');
6156
+
6157
+ for (let i = 0; i < lines.length; i++) {
6158
+ const line = lines[i];
6159
+
6160
+ // Function declarations
6161
+ const funcMatch = line.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
6162
+ if (funcMatch) {
6163
+ functions.push({
6164
+ name: funcMatch[1],
6165
+ lineNumber: i + 1
6166
+ });
6167
+ }
6168
+
6169
+ // Arrow functions assigned to variables
6170
+ const arrowMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*\([^)]*\)\s*=>/);
6171
+ if (arrowMatch) {
6172
+ functions.push({
6173
+ name: arrowMatch[1],
6174
+ lineNumber: i + 1
6175
+ });
6176
+ }
6177
+ }
6178
+
6179
+ return functions;
6180
+ }
6181
+
6182
+ parseJsVariables(jsContent) {
6183
+ const variables = [];
6184
+ const lines = jsContent.split('\n');
6185
+
6186
+ for (let i = 0; i < lines.length; i++) {
6187
+ const line = lines[i];
6188
+
6189
+ // Variable declarations
6190
+ const varMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
6191
+ if (varMatch && !line.includes('=') && !line.includes('function')) {
6192
+ variables.push({
6193
+ name: varMatch[1],
6194
+ lineNumber: i + 1
6195
+ });
6196
+ }
6197
+ }
6198
+
6199
+ return variables;
6200
+ }
6201
+
6202
+ isJsFunctionUsed(functionName, jsContent, htmlContent) {
6203
+ // Check if function is called in JS
6204
+ const jsCallPattern = new RegExp(`${functionName}\\s*\\(`, 'g');
6205
+ if (jsCallPattern.test(jsContent)) {
6206
+ return true;
6207
+ }
6208
+
6209
+ // Check if function is referenced in HTML (onclick, etc.)
6210
+ const htmlCallPattern = new RegExp(`${functionName}\\s*\\(`, 'g');
6211
+ if (htmlCallPattern.test(htmlContent)) {
6212
+ return true;
6213
+ }
6214
+
6215
+ // Check for event handlers in HTML
6216
+ const eventPattern = new RegExp(`on\\w+\\s*=\\s*["'][^"']*${functionName}`, 'g');
6217
+ if (eventPattern.test(htmlContent)) {
6218
+ return true;
6219
+ }
6220
+
6221
+ return false;
6222
+ }
6223
+
6224
+ isJsVariableUsed(variableName, jsContent, htmlContent) {
6225
+ // Create pattern that excludes the declaration line
6226
+ const lines = jsContent.split('\n');
6227
+ let usageFound = false;
6228
+
6229
+ for (let i = 0; i < lines.length; i++) {
6230
+ const line = lines[i];
6231
+
6232
+ // Skip the declaration line
6233
+ if (line.includes(`${variableName}`) &&
6234
+ (line.includes('const ') || line.includes('let ') || line.includes('var '))) {
6235
+ continue;
6236
+ }
6237
+
6238
+ // Check for usage
6239
+ const usagePattern = new RegExp(`\\b${variableName}\\b`);
6240
+ if (usagePattern.test(line)) {
6241
+ usageFound = true;
6242
+ break;
6243
+ }
6244
+ }
6245
+
6246
+ return usageFound;
6247
+ }
6248
+
6249
+ // Check file sizes and suggest optimizations
6250
+ async checkFileSizes(directory = '.') {
6251
+ console.log(chalk.blue('๐Ÿ“ Analyzing file sizes and suggesting optimizations...'));
6252
+
6253
+ const results = [];
6254
+ const allFiles = await this.findAllProjectFiles(directory);
6255
+ const sizeThresholds = {
6256
+ image: 500 * 1024, // 500KB for images
6257
+ // css: 100 * 1024, // 100KB for CSS
6258
+ js: 200 * 1024, // 200KB for JavaScript
6259
+ // html: 50 * 1024, // 50KB for HTML
6260
+ video: 10 * 1024 * 1024, // 10MB for videos
6261
+ audio: 5 * 1024 * 1024, // 5MB for audio
6262
+ other: 1 * 1024 * 1024 // 1MB for other files
6263
+ };
6264
+
6265
+ let totalSize = 0;
6266
+ const fileSizes = [];
6267
+
6268
+ for (const file of allFiles) {
6269
+ try {
6270
+ const stats = await require('fs').promises.stat(file);
6271
+ const fileSize = stats.size;
6272
+ const fileType = this.getFileCategory(file);
6273
+ const relativePath = path.relative(directory, file);
6274
+
6275
+ totalSize += fileSize;
6276
+ fileSizes.push({
6277
+ path: file,
6278
+ relativePath: relativePath,
6279
+ size: fileSize,
6280
+ type: fileType,
6281
+ sizeFormatted: this.formatFileSize(fileSize)
6282
+ });
6283
+
6284
+ // Check if file exceeds threshold
6285
+ const threshold = sizeThresholds[fileType] || sizeThresholds.other;
6286
+ if (fileSize > threshold) {
6287
+ const suggestions = this.getSizeOptimizationSuggestions(file, fileSize, fileType);
6288
+
6289
+ console.log(chalk.yellow(` ๐Ÿ“ Large ${fileType}: ${relativePath} (${this.formatFileSize(fileSize)})`));
6290
+ suggestions.forEach(suggestion => {
6291
+ console.log(chalk.gray(` ๐Ÿ’ก ${suggestion}`));
6292
+ });
6293
+
6294
+ results.push({
6295
+ type: `๐Ÿ“ Large ${fileType}`,
6296
+ description: `File size ${this.formatFileSize(fileSize)} exceeds recommended ${this.formatFileSize(threshold)}`,
6297
+ filePath: file,
6298
+ relativePath: relativePath,
6299
+ size: fileSize,
6300
+ sizeFormatted: this.formatFileSize(fileSize),
6301
+ threshold: threshold,
6302
+ thresholdFormatted: this.formatFileSize(threshold),
6303
+ suggestions: suggestions,
6304
+ fileType: fileType
6305
+ });
6306
+ }
6307
+ } catch (error) {
6308
+ // Skip files we can't read
6309
+ }
6310
+ }
6311
+
6312
+ // Sort files by size (largest first)
6313
+ fileSizes.sort((a, b) => b.size - a.size);
6314
+
6315
+ // Get type breakdown for return data only
6316
+ const typeBreakdown = this.getFileSizeBreakdown(fileSizes);
6317
+
6318
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${results.length} files that could be optimized`));
6319
+ console.log(chalk.gray('๐Ÿ’ก File size analysis is based on common best practices'));
6320
+
6321
+ return {
6322
+ largeFiles: results,
6323
+ allFiles: fileSizes,
6324
+ totalSize: totalSize,
6325
+ totalFiles: fileSizes.length,
6326
+ typeBreakdown: typeBreakdown
6327
+ };
6328
+ }
6329
+
6330
+ getFileCategory(filePath) {
6331
+ const ext = path.extname(filePath).toLowerCase();
6332
+
6333
+ if (['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.bmp'].includes(ext)) {
6334
+ return 'image';
6335
+ } else if (['.css', '.scss', '.sass', '.less'].includes(ext)) {
6336
+ return 'css';
6337
+ } else if (['.js', '.mjs', '.ts', '.jsx', '.tsx'].includes(ext)) {
6338
+ return 'js';
6339
+ } else if (['.html', '.htm', '.xhtml'].includes(ext)) {
6340
+ return 'html';
6341
+ } else if (['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'].includes(ext)) {
6342
+ return 'video';
6343
+ } else if (['.mp3', '.wav', '.ogg', '.aac', '.flac'].includes(ext)) {
6344
+ return 'audio';
6345
+ } else if (['.pdf', '.doc', '.docx', '.txt'].includes(ext)) {
6346
+ return 'document';
6347
+ } else if (['.zip', '.rar', '.tar', '.gz', '.7z'].includes(ext)) {
6348
+ return 'archive';
6349
+ } else if (['.ttf', '.woff', '.woff2', '.otf', '.eot'].includes(ext)) {
6350
+ return 'font';
6351
+ } else {
6352
+ return 'other';
6353
+ }
6354
+ }
6355
+
6356
+ getFileIcon(fileType) {
6357
+ const icons = {
6358
+ image: '๐Ÿ–ผ๏ธ',
6359
+ css: '๐ŸŽจ',
6360
+ js: '๐Ÿ“œ',
6361
+ html: '๐Ÿ“„',
6362
+ video: '๐ŸŽฅ',
6363
+ audio: '๐ŸŽต',
6364
+ document: '๐Ÿ“ƒ',
6365
+ archive: '๐Ÿ“ฆ',
6366
+ font: '๐Ÿ”ค',
6367
+ other: '๐Ÿ“„'
6368
+ };
6369
+ return icons[fileType] || '๐Ÿ“„';
6370
+ }
6371
+
6372
+ formatFileSize(bytes) {
6373
+ if (bytes === 0) return '0 B';
6374
+
6375
+ const k = 1024;
6376
+ const sizes = ['B', 'KB', 'MB', 'GB'];
6377
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
6378
+
6379
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
6380
+ }
6381
+
6382
+ getSizeOptimizationSuggestions(filePath, fileSize, fileType) {
6383
+ const suggestions = [];
6384
+ const fileName = path.basename(filePath);
6385
+ const ext = path.extname(filePath).toLowerCase();
6386
+
6387
+ switch (fileType) {
6388
+ case 'image':
6389
+ if (['.jpg', '.jpeg'].includes(ext)) {
6390
+ suggestions.push('Compress JPEG with tools like ImageOptim, TinyPNG, or jpegoptim');
6391
+ suggestions.push('Consider converting to WebP format for better compression');
6392
+ if (fileSize > 1024 * 1024) {
6393
+ suggestions.push('Resize image dimensions if currently larger than needed');
6394
+ }
6395
+ } else if (ext === '.png') {
6396
+ suggestions.push('Compress PNG with tools like OptiPNG, PNGCrush, or TinyPNG');
6397
+ suggestions.push('Consider converting to WebP for smaller size');
6398
+ suggestions.push('Use JPEG for photos if transparency not needed');
6399
+ } else if (ext === '.svg') {
6400
+ suggestions.push('Minify SVG code with SVGO or similar tools');
6401
+ suggestions.push('Remove unnecessary metadata and comments');
6402
+ } else if (ext === '.gif') {
6403
+ suggestions.push('Consider converting to WebP or MP4 for animations');
6404
+ suggestions.push('Reduce color palette if possible');
6405
+ }
6406
+ break;
6407
+
6408
+ case 'js':
6409
+ suggestions.push('Minify JavaScript code with tools like UglifyJS or Terser');
6410
+ suggestions.push('Enable gzip/brotli compression on your web server');
6411
+ suggestions.push('Consider code splitting for large bundles');
6412
+ suggestions.push('Remove unused code and dead code elimination');
6413
+ if (fileSize > 500 * 1024) {
6414
+ suggestions.push('Consider breaking into smaller modules');
6415
+ }
6416
+ break;
6417
+
6418
+ case 'css':
6419
+ suggestions.push('Minify CSS with tools like CleanCSS or cssnano');
6420
+ suggestions.push('Remove unused CSS rules (run --dead-code analysis)');
6421
+ suggestions.push('Enable gzip/brotli compression on your web server');
6422
+ suggestions.push('Consider using CSS-in-JS or CSS modules for better tree-shaking');
6423
+ break;
6424
+
6425
+ case 'html':
6426
+ suggestions.push('Minify HTML by removing whitespace and comments');
6427
+ suggestions.push('Enable gzip/brotli compression on your web server');
6428
+ suggestions.push('Inline critical CSS for above-the-fold content');
6429
+ break;
6430
+
6431
+ case 'video':
6432
+ suggestions.push('Compress video with H.264 or H.265 codecs');
6433
+ suggestions.push('Consider multiple quality versions (720p, 1080p)');
6434
+ suggestions.push('Use streaming formats like HLS or DASH for large videos');
6435
+ suggestions.push('Consider hosting on CDN or video platforms');
6436
+ break;
6437
+
6438
+ case 'audio':
6439
+ suggestions.push('Use compressed formats like MP3 or AAC instead of WAV');
6440
+ suggestions.push('Reduce bitrate if quality allows (128-192 kbps often sufficient)');
6441
+ suggestions.push('Consider streaming for long audio files');
6442
+ break;
6443
+
6444
+ case 'font':
6445
+ suggestions.push('Use WOFF2 format for better compression');
6446
+ suggestions.push('Subset fonts to include only needed characters');
6447
+ suggestions.push('Consider using system fonts or web font alternatives');
6448
+ break;
6449
+
6450
+ case 'document':
6451
+ if (ext === '.pdf') {
6452
+ suggestions.push('Compress PDF with tools like Ghostscript or online compressors');
6453
+ suggestions.push('Reduce image quality in PDF if acceptable');
6454
+ }
6455
+ break;
6456
+
6457
+ default:
6458
+ suggestions.push('Enable compression on your web server');
6459
+ suggestions.push('Consider if this file is necessary in production');
6460
+ }
6461
+
6462
+ // General suggestions for all large files
6463
+ if (fileSize > 1024 * 1024) {
6464
+ suggestions.push('Consider lazy loading if not needed immediately');
6465
+ suggestions.push('Use CDN for better delivery performance');
6466
+ }
6467
+
6468
+ return suggestions;
6469
+ }
6470
+
6471
+ getFileSizeBreakdown(fileSizes) {
6472
+ const breakdown = {};
6473
+
6474
+ fileSizes.forEach(file => {
6475
+ if (!breakdown[file.type]) {
6476
+ breakdown[file.type] = {
6477
+ count: 0,
6478
+ totalSize: 0
6479
+ };
6480
+ }
6481
+ breakdown[file.type].count++;
6482
+ breakdown[file.type].totalSize += file.size;
6483
+ });
6484
+
6485
+ // Sort by total size
6486
+ const sortedBreakdown = {};
6487
+ Object.entries(breakdown)
6488
+ .sort(([,a], [,b]) => b.totalSize - a.totalSize)
6489
+ .forEach(([key, value]) => {
6490
+ sortedBreakdown[key] = value;
6491
+ });
6492
+
6493
+ return sortedBreakdown;
5122
6494
  }
5123
6495
  }
5124
6496