gbu-accessibility-package 3.4.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/CHANGELOG.md +69 -0
- package/README-vi.md +63 -10
- package/README.md +62 -10
- package/cli.js +151 -14
- package/demo/1mb-jpg-example-file.jpg +0 -0
- package/demo/advanced-test.html +13 -13
- package/demo/aria-label-test.html +2 -2
- package/demo/broken-links-test.html +10 -10
- package/demo/comprehensive-test.html +4 -4
- package/demo/dead-code-test.css +68 -0
- package/demo/dead-code-test.html +36 -0
- package/demo/dead-code-test.js +77 -0
- package/demo/duplicate-roles.html +15 -15
- package/demo/enhanced-alt-test.html +22 -22
- package/demo/form-labels-test.html +17 -17
- package/demo/heading-structure-test.html +60 -0
- package/demo/heading-structure-test.html.backup +60 -0
- package/demo/large-file-demo.css +213 -0
- package/demo/nested-controls-test.html +17 -17
- package/demo/sample.html +12 -12
- package/demo/test-external-links.html +26 -0
- package/demo/unused-files-test.html +31 -0
- package/demo/unused-image.png +1 -0
- package/demo/unused-page.html +11 -0
- package/demo/unused-script.js +12 -0
- package/demo/unused-style.css +10 -0
- package/demo/very-large-file.js +2 -0
- package/lib/fixer.js +2094 -119
- package/package.json +15 -2
package/lib/fixer.js
CHANGED
|
@@ -1236,6 +1236,9 @@ class AccessibilityFixer {
|
|
|
1236
1236
|
altCreativity: config.altCreativity || 'balanced', // conservative, balanced, creative
|
|
1237
1237
|
includeEmotions: config.includeEmotions || false,
|
|
1238
1238
|
strictAltChecking: config.strictAltChecking || false,
|
|
1239
|
+
// New options for advanced features
|
|
1240
|
+
autoFixHeadings: config.autoFixHeadings || false, // Enable automatic heading fixes
|
|
1241
|
+
fixDescriptionLists: config.fixDescriptionLists || true, // Enable DL structure fixes
|
|
1239
1242
|
...config
|
|
1240
1243
|
};
|
|
1241
1244
|
|
|
@@ -2271,10 +2274,13 @@ class AccessibilityFixer {
|
|
|
2271
2274
|
console.log(chalk.yellow('\n๐ Step 8: Heading analysis...'));
|
|
2272
2275
|
results.headings = await this.analyzeHeadings(directory);
|
|
2273
2276
|
|
|
2274
|
-
// Step 9: Check broken links (no auto-fix)
|
|
2275
|
-
console.log(chalk.yellow('\n๐ Step 9:
|
|
2277
|
+
// Step 9: Check broken links and missing resources (no auto-fix)
|
|
2278
|
+
console.log(chalk.yellow('\n๐ Step 9: External links check...'));
|
|
2276
2279
|
results.brokenLinks = await this.checkBrokenLinks(directory);
|
|
2277
2280
|
|
|
2281
|
+
console.log(chalk.yellow('\n๐ Step 9b: Missing resources check...'));
|
|
2282
|
+
results.missingResources = await this.check404Resources(directory);
|
|
2283
|
+
|
|
2278
2284
|
// Step 10: Cleanup duplicate roles
|
|
2279
2285
|
console.log(chalk.yellow('\n๐งน Step 10: Cleanup duplicate roles...'));
|
|
2280
2286
|
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
@@ -2863,9 +2869,9 @@ class AccessibilityFixer {
|
|
|
2863
2869
|
return fixed;
|
|
2864
2870
|
}
|
|
2865
2871
|
|
|
2866
|
-
// Check for broken links
|
|
2872
|
+
// Check for broken external links only
|
|
2867
2873
|
async checkBrokenLinks(directory = '.') {
|
|
2868
|
-
console.log(chalk.blue('๐ Checking for broken links
|
|
2874
|
+
console.log(chalk.blue('๐ Checking for broken external links...'));
|
|
2869
2875
|
|
|
2870
2876
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2871
2877
|
const results = [];
|
|
@@ -2873,7 +2879,7 @@ class AccessibilityFixer {
|
|
|
2873
2879
|
for (const file of htmlFiles) {
|
|
2874
2880
|
try {
|
|
2875
2881
|
const content = await fs.readFile(file, 'utf8');
|
|
2876
|
-
const issues = await this.analyzeBrokenLinks(content, file);
|
|
2882
|
+
const issues = await this.analyzeBrokenLinks(content, file, 'external-only');
|
|
2877
2883
|
|
|
2878
2884
|
if (issues.length > 0) {
|
|
2879
2885
|
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
@@ -2892,12 +2898,46 @@ class AccessibilityFixer {
|
|
|
2892
2898
|
}
|
|
2893
2899
|
}
|
|
2894
2900
|
|
|
2895
|
-
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`));
|
|
2896
2902
|
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
2897
2903
|
return results;
|
|
2898
2904
|
}
|
|
2899
2905
|
|
|
2900
|
-
|
|
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') {
|
|
2901
2941
|
const issues = [];
|
|
2902
2942
|
const path = require('path');
|
|
2903
2943
|
const http = require('http');
|
|
@@ -2905,20 +2945,33 @@ class AccessibilityFixer {
|
|
|
2905
2945
|
const { URL } = require('url');
|
|
2906
2946
|
|
|
2907
2947
|
// Extract all links and resources
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
//
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
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
|
+
}
|
|
2922
2975
|
|
|
2923
2976
|
const baseDir = path.dirname(filePath);
|
|
2924
2977
|
|
|
@@ -2926,7 +2979,7 @@ class AccessibilityFixer {
|
|
|
2926
2979
|
let match;
|
|
2927
2980
|
while ((match = linkPattern.pattern.exec(content)) !== null) {
|
|
2928
2981
|
const url = match[1];
|
|
2929
|
-
const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element);
|
|
2982
|
+
const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element, mode);
|
|
2930
2983
|
if (issue) {
|
|
2931
2984
|
issues.push(issue);
|
|
2932
2985
|
}
|
|
@@ -2936,7 +2989,7 @@ class AccessibilityFixer {
|
|
|
2936
2989
|
return issues;
|
|
2937
2990
|
}
|
|
2938
2991
|
|
|
2939
|
-
async checkSingleLink(url, baseDir, resourceType, elementType) {
|
|
2992
|
+
async checkSingleLink(url, baseDir, resourceType, elementType, mode = 'all') {
|
|
2940
2993
|
// Skip certain URLs
|
|
2941
2994
|
if (this.shouldSkipUrl(url)) {
|
|
2942
2995
|
return null;
|
|
@@ -2944,10 +2997,16 @@ class AccessibilityFixer {
|
|
|
2944
2997
|
|
|
2945
2998
|
try {
|
|
2946
2999
|
if (this.isExternalUrl(url)) {
|
|
2947
|
-
// 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
|
+
}
|
|
2948
3004
|
return await this.checkExternalUrl(url, resourceType, elementType);
|
|
2949
3005
|
} else {
|
|
2950
|
-
// 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
|
+
}
|
|
2951
3010
|
return await this.checkLocalFile(url, baseDir, resourceType, elementType);
|
|
2952
3011
|
}
|
|
2953
3012
|
} catch (error) {
|
|
@@ -3609,12 +3668,18 @@ class AccessibilityFixer {
|
|
|
3609
3668
|
results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
3610
3669
|
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
3611
3670
|
|
|
3612
|
-
// Step 9: Broken links check
|
|
3613
|
-
console.log(chalk.blue('๐ Step
|
|
3671
|
+
// Step 9: Broken links and missing resources check
|
|
3672
|
+
console.log(chalk.blue('๐ Step 9a: External links check...'));
|
|
3614
3673
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
3615
3674
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3616
|
-
results.steps.push({ step:
|
|
3617
|
-
|
|
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'));
|
|
3618
3683
|
|
|
3619
3684
|
// Step 10: Cleanup duplicate roles
|
|
3620
3685
|
console.log(chalk.blue('๐งน Step 10: Cleanup duplicate roles...'));
|
|
@@ -3981,71 +4046,7 @@ class AccessibilityFixer {
|
|
|
3981
4046
|
return results;
|
|
3982
4047
|
}
|
|
3983
4048
|
|
|
3984
|
-
async checkBrokenLinks(directory = '.') {
|
|
3985
|
-
console.log(chalk.blue('๐ Checking for broken links and 404 resources...'));
|
|
3986
|
-
|
|
3987
|
-
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3988
|
-
const results = [];
|
|
3989
|
-
let totalIssuesFound = 0;
|
|
3990
|
-
|
|
3991
|
-
for (const file of htmlFiles) {
|
|
3992
|
-
try {
|
|
3993
|
-
const content = await fs.readFile(file, 'utf8');
|
|
3994
|
-
const issues = this.analyzeBrokenLinks(content, file);
|
|
3995
|
-
|
|
3996
|
-
if (issues.length > 0) {
|
|
3997
|
-
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
3998
|
-
issues.forEach(issue => {
|
|
3999
|
-
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4000
|
-
if (issue.suggestion) {
|
|
4001
|
-
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4002
|
-
}
|
|
4003
|
-
totalIssuesFound++;
|
|
4004
|
-
});
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
results.push({ file, status: 'analyzed', issues: issues.length });
|
|
4008
|
-
} catch (error) {
|
|
4009
|
-
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4010
|
-
results.push({ file, status: 'error', error: error.message });
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
|
|
4014
|
-
console.log(chalk.blue(`\n๐ Summary: Analyzed links in ${results.length} files`));
|
|
4015
|
-
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
4016
|
-
return results;
|
|
4017
|
-
}
|
|
4018
4049
|
|
|
4019
|
-
analyzeBrokenLinks(content, filePath) {
|
|
4020
|
-
const issues = [];
|
|
4021
|
-
|
|
4022
|
-
// Check for local image files
|
|
4023
|
-
const imgPattern = /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
4024
|
-
let match;
|
|
4025
|
-
|
|
4026
|
-
while ((match = imgPattern.exec(content)) !== null) {
|
|
4027
|
-
const src = match[1];
|
|
4028
|
-
|
|
4029
|
-
// Skip external URLs and data URLs
|
|
4030
|
-
if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('//')) {
|
|
4031
|
-
continue;
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
// Check if local file exists
|
|
4035
|
-
const fullPath = path.resolve(path.dirname(filePath), src);
|
|
4036
|
-
try {
|
|
4037
|
-
require('fs').statSync(fullPath);
|
|
4038
|
-
} catch (error) {
|
|
4039
|
-
issues.push({
|
|
4040
|
-
type: '๐ Image not found',
|
|
4041
|
-
description: `img file does not exist: ${src}`,
|
|
4042
|
-
suggestion: 'Create the missing file or update the image path'
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
|
|
4047
|
-
return issues;
|
|
4048
|
-
}
|
|
4049
4050
|
|
|
4050
4051
|
async cleanupDuplicateRoles(directory = '.') {
|
|
4051
4052
|
console.log(chalk.blue('๐งน Cleaning up duplicate role attributes...'));
|
|
@@ -4400,12 +4401,18 @@ class AccessibilityFixer {
|
|
|
4400
4401
|
results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4401
4402
|
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
4402
4403
|
|
|
4403
|
-
// Step 10: Broken links check
|
|
4404
|
-
console.log(chalk.blue('๐ Step
|
|
4404
|
+
// Step 10: Broken links and missing resources check
|
|
4405
|
+
console.log(chalk.blue('๐ Step 10a: External links check...'));
|
|
4405
4406
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4406
4407
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4407
|
-
results.steps.push({ step:
|
|
4408
|
-
|
|
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'));
|
|
4409
4416
|
|
|
4410
4417
|
// Step 11: Cleanup duplicate roles
|
|
4411
4418
|
console.log(chalk.blue('๐งน Step 11: Cleanup duplicate roles...'));
|
|
@@ -4454,37 +4461,2005 @@ class AccessibilityFixer {
|
|
|
4454
4461
|
}
|
|
4455
4462
|
}
|
|
4456
4463
|
|
|
4457
|
-
async
|
|
4458
|
-
|
|
4464
|
+
async fixHeadingStructure(directory = '.') {
|
|
4465
|
+
console.log(chalk.blue('๐ Fixing heading structure...'));
|
|
4459
4466
|
|
|
4460
|
-
|
|
4461
|
-
const
|
|
4467
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4468
|
+
const results = [];
|
|
4469
|
+
let totalIssuesFound = 0;
|
|
4470
|
+
let totalIssuesFixed = 0;
|
|
4462
4471
|
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4472
|
+
for (const file of htmlFiles) {
|
|
4473
|
+
try {
|
|
4474
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4475
|
+
const analysis = this.analyzeHeadingStructure(content);
|
|
4476
|
+
|
|
4477
|
+
if (analysis.issues.length > 0) {
|
|
4478
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4479
|
+
analysis.issues.forEach(issue => {
|
|
4480
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4481
|
+
if (issue.suggestion) {
|
|
4482
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4483
|
+
}
|
|
4484
|
+
totalIssuesFound++;
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
let fixed = content;
|
|
4489
|
+
let changesMade = false;
|
|
4490
|
+
|
|
4491
|
+
if (this.config.autoFixHeadings) {
|
|
4492
|
+
fixed = this.fixHeadingStructureInContent(content, analysis);
|
|
4493
|
+
changesMade = fixed !== content;
|
|
4494
|
+
|
|
4495
|
+
if (changesMade) {
|
|
4496
|
+
const fixedCount = this.countHeadingFixes(content, fixed);
|
|
4497
|
+
totalIssuesFixed += fixedCount;
|
|
4498
|
+
|
|
4499
|
+
if (this.config.backupFiles) {
|
|
4500
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
if (!this.config.dryRun) {
|
|
4504
|
+
await fs.writeFile(file, fixed);
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
console.log(chalk.green(`โ
Fixed heading structure in: ${file} (${fixedCount} fixes)`));
|
|
4508
|
+
results.push({ file, status: 'fixed', issues: analysis.issues.length, fixes: fixedCount });
|
|
4509
|
+
} else {
|
|
4510
|
+
results.push({ file, status: 'no-change', issues: analysis.issues.length });
|
|
4511
|
+
}
|
|
4512
|
+
} else {
|
|
4513
|
+
results.push({ file, status: 'analyzed', issues: analysis.issues.length });
|
|
4514
|
+
}
|
|
4515
|
+
} catch (error) {
|
|
4516
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4517
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4467
4518
|
}
|
|
4468
|
-
return files;
|
|
4469
4519
|
}
|
|
4470
4520
|
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4521
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} heading issues across ${results.length} files`));
|
|
4522
|
+
if (this.config.autoFixHeadings) {
|
|
4523
|
+
console.log(chalk.green(` Fixed ${totalIssuesFixed} heading issues automatically`));
|
|
4524
|
+
} else {
|
|
4525
|
+
console.log(chalk.gray('๐ก Use --auto-fix-headings option to enable automatic fixes'));
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
return results;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
analyzeHeadingStructure(content) {
|
|
4532
|
+
const issues = [];
|
|
4533
|
+
const suggestions = [];
|
|
4534
|
+
|
|
4535
|
+
// Extract all headings with their levels, text, and positions
|
|
4536
|
+
const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
4537
|
+
const headings = [];
|
|
4538
|
+
let match;
|
|
4539
|
+
|
|
4540
|
+
while ((match = headingPattern.exec(content)) !== null) {
|
|
4541
|
+
const level = parseInt(match[1]);
|
|
4542
|
+
const rawText = match[2];
|
|
4543
|
+
const text = rawText.replace(/<[^>]*>/g, '').trim();
|
|
4544
|
+
const fullTag = match[0];
|
|
4474
4545
|
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4546
|
+
headings.push({
|
|
4547
|
+
level,
|
|
4548
|
+
text,
|
|
4549
|
+
rawText,
|
|
4550
|
+
fullTag,
|
|
4551
|
+
position: match.index,
|
|
4552
|
+
originalMatch: match[0]
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
if (headings.length === 0) {
|
|
4557
|
+
issues.push({
|
|
4558
|
+
type: '๐ No headings found',
|
|
4559
|
+
description: 'Page has no heading elements',
|
|
4560
|
+
suggestion: 'Add heading elements (h1-h6) to structure content',
|
|
4561
|
+
severity: 'error',
|
|
4562
|
+
fixable: false
|
|
4563
|
+
});
|
|
4564
|
+
return { issues, suggestions, headings };
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
// Check for h1
|
|
4568
|
+
const h1Count = headings.filter(h => h.level === 1).length;
|
|
4569
|
+
if (h1Count === 0) {
|
|
4570
|
+
issues.push({
|
|
4571
|
+
type: '๐ Missing h1',
|
|
4572
|
+
description: 'Page should have exactly one h1 element',
|
|
4573
|
+
suggestion: 'Add an h1 element as the main page heading',
|
|
4574
|
+
severity: 'error',
|
|
4575
|
+
fixable: true,
|
|
4576
|
+
fix: 'add-h1'
|
|
4577
|
+
});
|
|
4578
|
+
} else if (h1Count > 1) {
|
|
4579
|
+
issues.push({
|
|
4580
|
+
type: '๐ Multiple h1 elements',
|
|
4581
|
+
description: `Found ${h1Count} h1 elements, should have only one`,
|
|
4582
|
+
suggestion: 'Convert extra h1 elements to h2-h6 as appropriate',
|
|
4583
|
+
severity: 'error',
|
|
4584
|
+
fixable: true,
|
|
4585
|
+
fix: 'fix-multiple-h1'
|
|
4586
|
+
});
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
// Check heading order and hierarchy
|
|
4590
|
+
for (let i = 1; i < headings.length; i++) {
|
|
4591
|
+
const current = headings[i];
|
|
4592
|
+
const previous = headings[i - 1];
|
|
4593
|
+
|
|
4594
|
+
// Check for level skipping
|
|
4595
|
+
if (current.level > previous.level + 1) {
|
|
4596
|
+
issues.push({
|
|
4597
|
+
type: '๐ Heading level skip',
|
|
4598
|
+
description: `Heading level jumps from h${previous.level} to h${current.level}`,
|
|
4599
|
+
suggestion: `Use h${previous.level + 1} instead of h${current.level}`,
|
|
4600
|
+
severity: 'warning',
|
|
4601
|
+
fixable: true,
|
|
4602
|
+
fix: 'fix-level-skip',
|
|
4603
|
+
targetIndex: i,
|
|
4604
|
+
correctLevel: previous.level + 1
|
|
4605
|
+
});
|
|
4483
4606
|
}
|
|
4484
4607
|
}
|
|
4485
4608
|
|
|
4486
|
-
|
|
4487
|
-
|
|
4609
|
+
// Check for empty headings
|
|
4610
|
+
headings.forEach((heading, index) => {
|
|
4611
|
+
if (!heading.text) {
|
|
4612
|
+
issues.push({
|
|
4613
|
+
type: '๐ Empty heading',
|
|
4614
|
+
description: `Heading ${index + 1} (h${heading.level}) is empty`,
|
|
4615
|
+
suggestion: 'Add descriptive text to the heading or remove it',
|
|
4616
|
+
severity: 'error',
|
|
4617
|
+
fixable: true,
|
|
4618
|
+
fix: 'fix-empty-heading',
|
|
4619
|
+
targetIndex: index
|
|
4620
|
+
});
|
|
4621
|
+
}
|
|
4622
|
+
});
|
|
4623
|
+
|
|
4624
|
+
// Check for consecutive headings with same level and similar content
|
|
4625
|
+
for (let i = 1; i < headings.length; i++) {
|
|
4626
|
+
const current = headings[i];
|
|
4627
|
+
const previous = headings[i - 1];
|
|
4628
|
+
|
|
4629
|
+
if (current.level === previous.level &&
|
|
4630
|
+
current.text.toLowerCase() === previous.text.toLowerCase() &&
|
|
4631
|
+
current.text.length > 0) {
|
|
4632
|
+
issues.push({
|
|
4633
|
+
type: '๐ Duplicate heading',
|
|
4634
|
+
description: `Duplicate h${current.level} heading: "${current.text}"`,
|
|
4635
|
+
suggestion: 'Make heading text unique or merge content',
|
|
4636
|
+
severity: 'warning',
|
|
4637
|
+
fixable: false
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
return { issues, suggestions, headings };
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
fixHeadingStructureInContent(content, analysis) {
|
|
4646
|
+
let fixed = content;
|
|
4647
|
+
const { issues, headings } = analysis;
|
|
4648
|
+
|
|
4649
|
+
// Sort issues by position (descending) to avoid position shifts
|
|
4650
|
+
const fixableIssues = issues
|
|
4651
|
+
.filter(issue => issue.fixable)
|
|
4652
|
+
.sort((a, b) => (b.targetIndex || 0) - (a.targetIndex || 0));
|
|
4653
|
+
|
|
4654
|
+
fixableIssues.forEach(issue => {
|
|
4655
|
+
switch (issue.fix) {
|
|
4656
|
+
case 'add-h1':
|
|
4657
|
+
fixed = this.addMissingH1(fixed);
|
|
4658
|
+
break;
|
|
4659
|
+
|
|
4660
|
+
case 'fix-multiple-h1':
|
|
4661
|
+
fixed = this.fixMultipleH1(fixed, headings);
|
|
4662
|
+
break;
|
|
4663
|
+
|
|
4664
|
+
case 'fix-level-skip':
|
|
4665
|
+
if (issue.targetIndex !== undefined && issue.correctLevel) {
|
|
4666
|
+
fixed = this.fixHeadingLevelSkip(fixed, headings[issue.targetIndex], issue.correctLevel);
|
|
4667
|
+
}
|
|
4668
|
+
break;
|
|
4669
|
+
|
|
4670
|
+
case 'fix-empty-heading':
|
|
4671
|
+
if (issue.targetIndex !== undefined) {
|
|
4672
|
+
fixed = this.fixEmptyHeading(fixed, headings[issue.targetIndex]);
|
|
4673
|
+
}
|
|
4674
|
+
break;
|
|
4675
|
+
}
|
|
4676
|
+
});
|
|
4677
|
+
|
|
4678
|
+
return fixed;
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
addMissingH1(content) {
|
|
4682
|
+
// Try to find the first heading and convert it to h1
|
|
4683
|
+
const firstHeadingMatch = content.match(/<h([2-6])[^>]*>([\s\S]*?)<\/h[2-6]>/i);
|
|
4684
|
+
|
|
4685
|
+
if (firstHeadingMatch) {
|
|
4686
|
+
const level = firstHeadingMatch[1];
|
|
4687
|
+
const replacement = firstHeadingMatch[0].replace(
|
|
4688
|
+
new RegExp(`<h${level}([^>]*)>`, 'i'),
|
|
4689
|
+
'<h1$1>'
|
|
4690
|
+
).replace(
|
|
4691
|
+
new RegExp(`</h${level}>`, 'i'),
|
|
4692
|
+
'</h1>'
|
|
4693
|
+
);
|
|
4694
|
+
|
|
4695
|
+
const result = content.replace(firstHeadingMatch[0], replacement);
|
|
4696
|
+
console.log(chalk.yellow(` ๐ Converted first h${level} to h1`));
|
|
4697
|
+
return result;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
// If no headings found, try to add h1 based on title or first significant text
|
|
4701
|
+
const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
4702
|
+
if (titleMatch) {
|
|
4703
|
+
const title = titleMatch[1].trim();
|
|
4704
|
+
// Insert h1 after opening body tag
|
|
4705
|
+
const bodyMatch = content.match(/(<body[^>]*>)/i);
|
|
4706
|
+
if (bodyMatch) {
|
|
4707
|
+
const h1Element = `\n <h1>${title}</h1>\n`;
|
|
4708
|
+
const result = content.replace(bodyMatch[1], bodyMatch[1] + h1Element);
|
|
4709
|
+
console.log(chalk.yellow(` ๐ Added h1 element with title: "${title}"`));
|
|
4710
|
+
return result;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
return content;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
fixMultipleH1(content, headings) {
|
|
4718
|
+
const h1Elements = headings.filter(h => h.level === 1);
|
|
4719
|
+
|
|
4720
|
+
// Keep the first h1, convert others to h2
|
|
4721
|
+
for (let i = 1; i < h1Elements.length; i++) {
|
|
4722
|
+
const h1 = h1Elements[i];
|
|
4723
|
+
const replacement = h1.fullTag.replace(/<h1([^>]*)>/i, '<h2$1>').replace(/<\/h1>/i, '</h2>');
|
|
4724
|
+
content = content.replace(h1.fullTag, replacement);
|
|
4725
|
+
console.log(chalk.yellow(` ๐ Converted extra h1 to h2: "${h1.text}"`));
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
return content;
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
fixHeadingLevelSkip(content, heading, correctLevel) {
|
|
4732
|
+
const replacement = heading.fullTag
|
|
4733
|
+
.replace(new RegExp(`<h${heading.level}([^>]*)>`, 'i'), `<h${correctLevel}$1>`)
|
|
4734
|
+
.replace(new RegExp(`</h${heading.level}>`, 'i'), `</h${correctLevel}>`);
|
|
4735
|
+
|
|
4736
|
+
const result = content.replace(heading.fullTag, replacement);
|
|
4737
|
+
console.log(chalk.yellow(` ๐ Fixed level skip: h${heading.level} โ h${correctLevel} for "${heading.text}"`));
|
|
4738
|
+
return result;
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
fixEmptyHeading(content, heading) {
|
|
4742
|
+
// Generate meaningful text based on context
|
|
4743
|
+
const contextText = this.generateHeadingText(content, heading);
|
|
4744
|
+
|
|
4745
|
+
if (contextText) {
|
|
4746
|
+
const replacement = heading.fullTag.replace(
|
|
4747
|
+
/<h([1-6])([^>]*)>[\s\S]*?<\/h[1-6]>/i,
|
|
4748
|
+
`<h$1$2>${contextText}</h$1>`
|
|
4749
|
+
);
|
|
4750
|
+
|
|
4751
|
+
const result = content.replace(heading.fullTag, replacement);
|
|
4752
|
+
console.log(chalk.yellow(` ๐ Added text to empty heading: "${contextText}"`));
|
|
4753
|
+
return result;
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
// If can't generate text, remove the empty heading
|
|
4757
|
+
const result = content.replace(heading.fullTag, '');
|
|
4758
|
+
console.log(chalk.yellow(` ๐ Removed empty h${heading.level} heading`));
|
|
4759
|
+
return result;
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
generateHeadingText(content, heading) {
|
|
4763
|
+
const lang = this.config.language;
|
|
4764
|
+
|
|
4765
|
+
// Try to find nearby text content
|
|
4766
|
+
const position = heading.position;
|
|
4767
|
+
const contextRange = 500;
|
|
4768
|
+
const beforeContext = content.substring(Math.max(0, position - contextRange), position);
|
|
4769
|
+
const afterContext = content.substring(position + heading.fullTag.length, position + heading.fullTag.length + contextRange);
|
|
4770
|
+
|
|
4771
|
+
// Look for meaningful text in nearby paragraphs
|
|
4772
|
+
const nearbyText = (beforeContext + afterContext).replace(/<[^>]*>/g, ' ').trim();
|
|
4773
|
+
const words = nearbyText.split(/\s+/).filter(word => word.length > 2);
|
|
4774
|
+
|
|
4775
|
+
if (words.length > 0) {
|
|
4776
|
+
const meaningfulWords = words.slice(0, 3);
|
|
4777
|
+
return meaningfulWords.join(' ');
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
// Fallback to generic text based on language
|
|
4781
|
+
const genericTexts = {
|
|
4782
|
+
ja: ['่ฆๅบใ', 'ใปใฏใทใงใณ', 'ใณใณใใณใ'],
|
|
4783
|
+
en: ['Heading', 'Section', 'Content'],
|
|
4784
|
+
vi: ['Tiรชu ฤแป', 'Phแบงn', 'Nแปi dung']
|
|
4785
|
+
};
|
|
4786
|
+
|
|
4787
|
+
const texts = genericTexts[lang] || genericTexts.en;
|
|
4788
|
+
return texts[0];
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
countHeadingFixes(originalContent, fixedContent) {
|
|
4792
|
+
// Count the number of heading-related changes
|
|
4793
|
+
const originalHeadings = (originalContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
|
|
4794
|
+
const fixedHeadings = (fixedContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
|
|
4795
|
+
|
|
4796
|
+
// Simple heuristic: count tag changes
|
|
4797
|
+
let changes = 0;
|
|
4798
|
+
|
|
4799
|
+
// Count h1 additions
|
|
4800
|
+
const originalH1 = (originalContent.match(/<h1[^>]*>/gi) || []).length;
|
|
4801
|
+
const fixedH1 = (fixedContent.match(/<h1[^>]*>/gi) || []).length;
|
|
4802
|
+
changes += Math.abs(fixedH1 - originalH1);
|
|
4803
|
+
|
|
4804
|
+
// Count level changes (rough estimate)
|
|
4805
|
+
for (let level = 1; level <= 6; level++) {
|
|
4806
|
+
const originalCount = (originalContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
|
|
4807
|
+
const fixedCount = (fixedContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
|
|
4808
|
+
changes += Math.abs(fixedCount - originalCount);
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
return Math.max(1, Math.floor(changes / 2)); // Rough estimate
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
async fixDescriptionLists(directory = '.') {
|
|
4815
|
+
console.log(chalk.blue('๐ Fixing description list structure...'));
|
|
4816
|
+
|
|
4817
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4818
|
+
const results = [];
|
|
4819
|
+
let totalIssuesFound = 0;
|
|
4820
|
+
|
|
4821
|
+
for (const file of htmlFiles) {
|
|
4822
|
+
try {
|
|
4823
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4824
|
+
const issues = this.analyzeDescriptionListStructure(content);
|
|
4825
|
+
|
|
4826
|
+
if (issues.length > 0) {
|
|
4827
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4828
|
+
issues.forEach(issue => {
|
|
4829
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4830
|
+
if (issue.suggestion) {
|
|
4831
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4832
|
+
}
|
|
4833
|
+
totalIssuesFound++;
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
const fixed = this.fixDescriptionListStructureInContent(content);
|
|
4838
|
+
|
|
4839
|
+
if (fixed !== content) {
|
|
4840
|
+
if (this.config.backupFiles) {
|
|
4841
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
if (!this.config.dryRun) {
|
|
4845
|
+
await fs.writeFile(file, fixed);
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
console.log(chalk.green(`โ
Fixed description list structure in: ${file}`));
|
|
4849
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
4850
|
+
} else {
|
|
4851
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
4852
|
+
}
|
|
4853
|
+
} catch (error) {
|
|
4854
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4855
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} description list issues across ${results.length} files`));
|
|
4860
|
+
return results;
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
analyzeDescriptionListStructure(content) {
|
|
4864
|
+
const issues = [];
|
|
4865
|
+
|
|
4866
|
+
// Find all dl elements
|
|
4867
|
+
const dlPattern = /<dl[^>]*>([\s\S]*?)<\/dl>/gi;
|
|
4868
|
+
let dlMatch;
|
|
4869
|
+
let dlIndex = 0;
|
|
4870
|
+
|
|
4871
|
+
while ((dlMatch = dlPattern.exec(content)) !== null) {
|
|
4872
|
+
dlIndex++;
|
|
4873
|
+
const dlContent = dlMatch[1];
|
|
4874
|
+
const dlElement = dlMatch[0];
|
|
4875
|
+
|
|
4876
|
+
// Analyze the content inside dl
|
|
4877
|
+
const dtElements = (dlContent.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || []);
|
|
4878
|
+
const ddElements = (dlContent.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || []);
|
|
4879
|
+
|
|
4880
|
+
// Check for empty dl
|
|
4881
|
+
if (dtElements.length === 0 && ddElements.length === 0) {
|
|
4882
|
+
issues.push({
|
|
4883
|
+
type: '๐ Empty description list',
|
|
4884
|
+
description: `Description list ${dlIndex} is empty`,
|
|
4885
|
+
suggestion: 'Add dt/dd pairs or remove the empty dl element',
|
|
4886
|
+
severity: 'error',
|
|
4887
|
+
dlIndex,
|
|
4888
|
+
fix: 'remove-empty-dl'
|
|
4889
|
+
});
|
|
4890
|
+
continue;
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
// Check for missing dt elements
|
|
4894
|
+
if (dtElements.length === 0 && ddElements.length > 0) {
|
|
4895
|
+
issues.push({
|
|
4896
|
+
type: '๐ Missing dt elements',
|
|
4897
|
+
description: `Description list ${dlIndex} has dd elements but no dt elements`,
|
|
4898
|
+
suggestion: 'Add dt elements to describe the dd content',
|
|
4899
|
+
severity: 'error',
|
|
4900
|
+
dlIndex,
|
|
4901
|
+
fix: 'add-missing-dt'
|
|
4902
|
+
});
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// Check for missing dd elements
|
|
4906
|
+
if (dtElements.length > 0 && ddElements.length === 0) {
|
|
4907
|
+
issues.push({
|
|
4908
|
+
type: '๐ Missing dd elements',
|
|
4909
|
+
description: `Description list ${dlIndex} has dt elements but no dd elements`,
|
|
4910
|
+
suggestion: 'Add dd elements to provide descriptions',
|
|
4911
|
+
severity: 'error',
|
|
4912
|
+
dlIndex,
|
|
4913
|
+
fix: 'add-missing-dd'
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
// Check for improper nesting (non-dt/dd elements directly in dl)
|
|
4918
|
+
const invalidChildren = dlContent.match(/<(?!dt|dd|\/dt|\/dd)[a-zA-Z][^>]*>/g);
|
|
4919
|
+
if (invalidChildren) {
|
|
4920
|
+
const invalidTags = [...new Set(invalidChildren.map(tag => tag.match(/<([a-zA-Z]+)/)[1]))];
|
|
4921
|
+
issues.push({
|
|
4922
|
+
type: '๐ Invalid dl children',
|
|
4923
|
+
description: `Description list ${dlIndex} contains invalid child elements: ${invalidTags.join(', ')}`,
|
|
4924
|
+
suggestion: 'Only dt and dd elements should be direct children of dl',
|
|
4925
|
+
severity: 'warning',
|
|
4926
|
+
dlIndex,
|
|
4927
|
+
fix: 'wrap-invalid-children'
|
|
4928
|
+
});
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
// Check for empty dt/dd elements
|
|
4932
|
+
dtElements.forEach((dt, index) => {
|
|
4933
|
+
const dtText = dt.replace(/<[^>]*>/g, '').trim();
|
|
4934
|
+
if (!dtText) {
|
|
4935
|
+
issues.push({
|
|
4936
|
+
type: '๐ Empty dt element',
|
|
4937
|
+
description: `Empty dt element in description list ${dlIndex}`,
|
|
4938
|
+
suggestion: 'Add descriptive text to the dt element',
|
|
4939
|
+
severity: 'warning',
|
|
4940
|
+
dlIndex,
|
|
4941
|
+
dtIndex: index,
|
|
4942
|
+
fix: 'fix-empty-dt'
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
});
|
|
4946
|
+
|
|
4947
|
+
ddElements.forEach((dd, index) => {
|
|
4948
|
+
const ddText = dd.replace(/<[^>]*>/g, '').trim();
|
|
4949
|
+
if (!ddText) {
|
|
4950
|
+
issues.push({
|
|
4951
|
+
type: '๐ Empty dd element',
|
|
4952
|
+
description: `Empty dd element in description list ${dlIndex}`,
|
|
4953
|
+
suggestion: 'Add descriptive content to the dd element',
|
|
4954
|
+
severity: 'warning',
|
|
4955
|
+
dlIndex,
|
|
4956
|
+
ddIndex: index,
|
|
4957
|
+
fix: 'fix-empty-dd'
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
});
|
|
4961
|
+
|
|
4962
|
+
// Check for proper dt/dd pairing
|
|
4963
|
+
if (dtElements.length > 0 && ddElements.length > 0) {
|
|
4964
|
+
// Basic check: should have at least one dd for each dt
|
|
4965
|
+
if (ddElements.length < dtElements.length) {
|
|
4966
|
+
issues.push({
|
|
4967
|
+
type: '๐ Insufficient dd elements',
|
|
4968
|
+
description: `Description list ${dlIndex} has ${dtElements.length} dt elements but only ${ddElements.length} dd elements`,
|
|
4969
|
+
suggestion: 'Each dt should have at least one corresponding dd element',
|
|
4970
|
+
severity: 'warning',
|
|
4971
|
+
dlIndex
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
return issues;
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
fixDescriptionListStructureInContent(content) {
|
|
4981
|
+
let fixed = content;
|
|
4982
|
+
|
|
4983
|
+
// Fix empty dl elements
|
|
4984
|
+
fixed = fixed.replace(/<dl[^>]*>\s*<\/dl>/gi, (match) => {
|
|
4985
|
+
console.log(chalk.yellow(` ๐ Removed empty description list`));
|
|
4986
|
+
return '';
|
|
4987
|
+
});
|
|
4988
|
+
|
|
4989
|
+
// Fix dl elements with only whitespace
|
|
4990
|
+
fixed = fixed.replace(/<dl[^>]*>[\s\n\r]*<\/dl>/gi, (match) => {
|
|
4991
|
+
console.log(chalk.yellow(` ๐ Removed empty description list`));
|
|
4992
|
+
return '';
|
|
4993
|
+
});
|
|
4994
|
+
|
|
4995
|
+
// Fix dl elements with invalid direct children
|
|
4996
|
+
fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
|
|
4997
|
+
// Extract dt and dd elements
|
|
4998
|
+
const dtElements = content.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || [];
|
|
4999
|
+
const ddElements = content.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || [];
|
|
5000
|
+
|
|
5001
|
+
// Find invalid children (not dt or dd)
|
|
5002
|
+
let cleanContent = content;
|
|
5003
|
+
|
|
5004
|
+
// Remove invalid direct children by wrapping them in dd
|
|
5005
|
+
cleanContent = cleanContent.replace(/<(?!dt|dd|\/dt|\/dd)([a-zA-Z][^>]*)>([\s\S]*?)<\/[a-zA-Z]+>/gi, (invalidMatch, tag, innerContent) => {
|
|
5006
|
+
console.log(chalk.yellow(` ๐ Wrapped invalid child element in dd`));
|
|
5007
|
+
return `<dd>${invalidMatch}</dd>`;
|
|
5008
|
+
});
|
|
5009
|
+
|
|
5010
|
+
// Handle text nodes that are not in dt/dd
|
|
5011
|
+
cleanContent = cleanContent.replace(/^([^<]+)(?=<(?:dt|dd))/gm, (textMatch) => {
|
|
5012
|
+
const trimmed = textMatch.trim();
|
|
5013
|
+
if (trimmed) {
|
|
5014
|
+
console.log(chalk.yellow(` ๐ Wrapped loose text in dd`));
|
|
5015
|
+
return `<dd>${trimmed}</dd>`;
|
|
5016
|
+
}
|
|
5017
|
+
return '';
|
|
5018
|
+
});
|
|
5019
|
+
|
|
5020
|
+
return `<dl${attributes}>${cleanContent}</dl>`;
|
|
5021
|
+
});
|
|
5022
|
+
|
|
5023
|
+
// Add missing dd elements for dt elements without corresponding dd
|
|
5024
|
+
fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
|
|
5025
|
+
const dtPattern = /<dt[^>]*>([\s\S]*?)<\/dt>/gi;
|
|
5026
|
+
const ddPattern = /<dd[^>]*>[\s\S]*?<\/dd>/gi;
|
|
5027
|
+
|
|
5028
|
+
const dtMatches = [...content.matchAll(dtPattern)];
|
|
5029
|
+
const ddMatches = [...content.matchAll(ddPattern)];
|
|
5030
|
+
|
|
5031
|
+
if (dtMatches.length > 0 && ddMatches.length === 0) {
|
|
5032
|
+
// Add dd elements after each dt
|
|
5033
|
+
let fixedContent = content;
|
|
5034
|
+
|
|
5035
|
+
// Process from end to beginning to maintain positions
|
|
5036
|
+
for (let i = dtMatches.length - 1; i >= 0; i--) {
|
|
5037
|
+
const dtMatch = dtMatches[i];
|
|
5038
|
+
const dtText = dtMatch[1].replace(/<[^>]*>/g, '').trim();
|
|
5039
|
+
const ddText = this.generateDescriptionForTerm(dtText);
|
|
5040
|
+
|
|
5041
|
+
const insertPosition = dtMatch.index + dtMatch[0].length;
|
|
5042
|
+
fixedContent = fixedContent.slice(0, insertPosition) +
|
|
5043
|
+
`\n <dd>${ddText}</dd>` +
|
|
5044
|
+
fixedContent.slice(insertPosition);
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
console.log(chalk.yellow(` ๐ Added missing dd elements for ${dtMatches.length} dt elements`));
|
|
5048
|
+
return `<dl${attributes}>${fixedContent}</dl>`;
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
return match;
|
|
5052
|
+
});
|
|
5053
|
+
|
|
5054
|
+
// Fix empty dt/dd elements
|
|
5055
|
+
fixed = fixed.replace(/<dt[^>]*>\s*<\/dt>/gi, (match) => {
|
|
5056
|
+
const lang = this.config.language;
|
|
5057
|
+
const defaultText = lang === 'ja' ? '้
็ฎ' : lang === 'vi' ? 'Mแปฅc' : 'Term';
|
|
5058
|
+
console.log(chalk.yellow(` ๐ Added text to empty dt element`));
|
|
5059
|
+
return match.replace(/>\s*</, `>${defaultText}<`);
|
|
5060
|
+
});
|
|
5061
|
+
|
|
5062
|
+
fixed = fixed.replace(/<dd[^>]*>\s*<\/dd>/gi, (match) => {
|
|
5063
|
+
const lang = this.config.language;
|
|
5064
|
+
const defaultText = lang === 'ja' ? '่ชฌๆ' : lang === 'vi' ? 'Mรด tแบฃ' : 'Description';
|
|
5065
|
+
console.log(chalk.yellow(` ๐ Added text to empty dd element`));
|
|
5066
|
+
return match.replace(/>\s*</, `>${defaultText}<`);
|
|
5067
|
+
});
|
|
5068
|
+
|
|
5069
|
+
return fixed;
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
generateDescriptionForTerm(termText) {
|
|
5073
|
+
const lang = this.config.language;
|
|
5074
|
+
|
|
5075
|
+
// Try to generate meaningful description based on term
|
|
5076
|
+
if (termText) {
|
|
5077
|
+
const descriptions = {
|
|
5078
|
+
ja: `${termText}ใฎ่ชฌๆ`,
|
|
5079
|
+
en: `Description of ${termText}`,
|
|
5080
|
+
vi: `Mรด tแบฃ vแป ${termText}`
|
|
5081
|
+
};
|
|
5082
|
+
return descriptions[lang] || descriptions.en;
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
// Fallback to generic description
|
|
5086
|
+
const fallbacks = {
|
|
5087
|
+
ja: '่ชฌๆ',
|
|
5088
|
+
en: 'Description',
|
|
5089
|
+
vi: 'Mรด tแบฃ'
|
|
5090
|
+
};
|
|
5091
|
+
|
|
5092
|
+
return fallbacks[lang] || fallbacks.en;
|
|
5093
|
+
}
|
|
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
|
+
|
|
5470
|
+
async findHtmlFiles(directory) {
|
|
5471
|
+
const files = [];
|
|
5472
|
+
|
|
5473
|
+
// Check if the path is a file or directory
|
|
5474
|
+
const stat = await fs.stat(directory);
|
|
5475
|
+
|
|
5476
|
+
if (stat.isFile()) {
|
|
5477
|
+
// If it's a file, check if it's HTML
|
|
5478
|
+
if (directory.endsWith('.html')) {
|
|
5479
|
+
files.push(directory);
|
|
5480
|
+
}
|
|
5481
|
+
return files;
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
// If it's a directory, scan recursively
|
|
5485
|
+
async function scan(dir) {
|
|
5486
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5487
|
+
|
|
5488
|
+
for (const entry of entries) {
|
|
5489
|
+
const fullPath = path.join(dir, entry.name);
|
|
5490
|
+
|
|
5491
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
5492
|
+
await scan(fullPath);
|
|
5493
|
+
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
5494
|
+
files.push(fullPath);
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
await scan(directory);
|
|
5500
|
+
return files;
|
|
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;
|
|
4488
6463
|
}
|
|
4489
6464
|
}
|
|
4490
6465
|
|