gbu-accessibility-package 3.5.0 โ†’ 3.8.0

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