gbu-accessibility-package 3.8.8 → 3.9.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/README-vi.md +94 -0
- package/README.md +95 -1
- package/cli.js +128 -105
- package/lib/fixer.js +440 -99
- package/package.json +2 -1
package/lib/fixer.js
CHANGED
|
@@ -1260,7 +1260,7 @@ class AccessibilityFixer {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
|
|
1262
1262
|
async fixHtmlLang(directory = '.') {
|
|
1263
|
-
console.log(chalk.blue('📝
|
|
1263
|
+
console.log(chalk.blue('📝 Đang sửa thuộc tính HTML lang...'));
|
|
1264
1264
|
|
|
1265
1265
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1266
1266
|
const results = [];
|
|
@@ -1294,7 +1294,7 @@ class AccessibilityFixer {
|
|
|
1294
1294
|
}
|
|
1295
1295
|
|
|
1296
1296
|
async fixEmptyAltAttributes(directory = '.') {
|
|
1297
|
-
console.log(chalk.blue('🖼️
|
|
1297
|
+
console.log(chalk.blue('🖼️ Đang sửa thuộc tính alt rỗng...'));
|
|
1298
1298
|
|
|
1299
1299
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1300
1300
|
const results = [];
|
|
@@ -1369,7 +1369,7 @@ class AccessibilityFixer {
|
|
|
1369
1369
|
}
|
|
1370
1370
|
}
|
|
1371
1371
|
|
|
1372
|
-
console.log(chalk.blue(`\n📊
|
|
1372
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề thuộc tính alt trong ${results.length} file`));
|
|
1373
1373
|
if (this.config.enhancedAltMode) {
|
|
1374
1374
|
console.log(chalk.gray(` 🔍 Enhanced analysis mode: Comprehensive quality checking enabled`));
|
|
1375
1375
|
}
|
|
@@ -1406,7 +1406,7 @@ class AccessibilityFixer {
|
|
|
1406
1406
|
}
|
|
1407
1407
|
|
|
1408
1408
|
async fixRoleAttributes(directory = '.') {
|
|
1409
|
-
console.log(chalk.blue('🎭
|
|
1409
|
+
console.log(chalk.blue('🎭 Đang sửa thuộc tính role...'));
|
|
1410
1410
|
|
|
1411
1411
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1412
1412
|
const results = [];
|
|
@@ -1447,7 +1447,7 @@ class AccessibilityFixer {
|
|
|
1447
1447
|
}
|
|
1448
1448
|
}
|
|
1449
1449
|
|
|
1450
|
-
console.log(chalk.blue(`\n📊
|
|
1450
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề thuộc tính role trong ${results.length} file`));
|
|
1451
1451
|
return results;
|
|
1452
1452
|
}
|
|
1453
1453
|
|
|
@@ -1483,7 +1483,7 @@ class AccessibilityFixer {
|
|
|
1483
1483
|
}
|
|
1484
1484
|
}
|
|
1485
1485
|
|
|
1486
|
-
console.log(chalk.blue(`\n📊
|
|
1486
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề aria-label trong ${results.length} file`));
|
|
1487
1487
|
return results;
|
|
1488
1488
|
}
|
|
1489
1489
|
|
|
@@ -2275,7 +2275,7 @@ class AccessibilityFixer {
|
|
|
2275
2275
|
}
|
|
2276
2276
|
|
|
2277
2277
|
async cleanupDuplicateRoles(directory = '.') {
|
|
2278
|
-
console.log(chalk.blue('🧹
|
|
2278
|
+
console.log(chalk.blue('🧹 Đang dọn dẹp các thuộc tính role trùng lặp...'));
|
|
2279
2279
|
|
|
2280
2280
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2281
2281
|
const results = [];
|
|
@@ -2307,7 +2307,7 @@ class AccessibilityFixer {
|
|
|
2307
2307
|
}
|
|
2308
2308
|
}
|
|
2309
2309
|
|
|
2310
|
-
console.log(chalk.blue(`\n📊
|
|
2310
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã dọn dẹp role trùng lặp trong ${totalFixedFiles} file`));
|
|
2311
2311
|
return results;
|
|
2312
2312
|
}
|
|
2313
2313
|
|
|
@@ -2341,7 +2341,7 @@ class AccessibilityFixer {
|
|
|
2341
2341
|
}
|
|
2342
2342
|
|
|
2343
2343
|
async fixAllAccessibilityIssues(directory = '.') {
|
|
2344
|
-
console.log(chalk.blue('🚀
|
|
2344
|
+
console.log(chalk.blue('🚀 Đang bắt đầu sửa lỗi accessibility toàn diện...'));
|
|
2345
2345
|
|
|
2346
2346
|
const results = {
|
|
2347
2347
|
lang: [],
|
|
@@ -2358,46 +2358,46 @@ class AccessibilityFixer {
|
|
|
2358
2358
|
|
|
2359
2359
|
try {
|
|
2360
2360
|
// Step 1: Fix lang attributes
|
|
2361
|
-
console.log(chalk.yellow('\n📝
|
|
2361
|
+
console.log(chalk.yellow('\n📝 Bước 1: Thuộc tính HTML lang...'));
|
|
2362
2362
|
results.lang = await this.fixHtmlLang(directory);
|
|
2363
2363
|
|
|
2364
2364
|
// Step 2: Fix alt attributes
|
|
2365
|
-
console.log(chalk.yellow('\n🖼️
|
|
2365
|
+
console.log(chalk.yellow('\n🖼️ Bước 2: Thuộc tính alt...'));
|
|
2366
2366
|
results.alt = await this.fixEmptyAltAttributes(directory);
|
|
2367
2367
|
|
|
2368
2368
|
// Step 3: Fix role attributes
|
|
2369
|
-
console.log(chalk.yellow('\n🎭
|
|
2369
|
+
console.log(chalk.yellow('\n🎭 Bước 3: Thuộc tính role...'));
|
|
2370
2370
|
results.roles = await this.fixRoleAttributes(directory);
|
|
2371
2371
|
|
|
2372
2372
|
// Step 4: Fix form labels
|
|
2373
|
-
console.log(chalk.yellow('\n📋
|
|
2373
|
+
console.log(chalk.yellow('\n📋 Bước 4: Nhãn form...'));
|
|
2374
2374
|
results.forms = await this.fixFormLabels(directory);
|
|
2375
2375
|
|
|
2376
2376
|
// Step 5: Fix button names
|
|
2377
|
-
console.log(chalk.yellow('\n🔘
|
|
2377
|
+
console.log(chalk.yellow('\n🔘 Bước 5: Tên button...'));
|
|
2378
2378
|
results.buttons = await this.fixButtonNames(directory);
|
|
2379
2379
|
|
|
2380
2380
|
// Step 6: Fix link names
|
|
2381
|
-
console.log(chalk.yellow('\n🔗
|
|
2381
|
+
console.log(chalk.yellow('\n🔗 Bước 6: Tên link...'));
|
|
2382
2382
|
results.links = await this.fixLinkNames(directory);
|
|
2383
2383
|
|
|
2384
2384
|
// Step 7: Fix landmarks
|
|
2385
|
-
console.log(chalk.yellow('\n🏛️
|
|
2385
|
+
console.log(chalk.yellow('\n🏛️ Bước 7: Landmark...'));
|
|
2386
2386
|
results.landmarks = await this.fixLandmarks(directory);
|
|
2387
2387
|
|
|
2388
2388
|
// Step 8: Analyze headings (no auto-fix)
|
|
2389
|
-
console.log(chalk.yellow('\n📑
|
|
2389
|
+
console.log(chalk.yellow('\n📑 Bước 8: Phân tích heading...'));
|
|
2390
2390
|
results.headings = await this.analyzeHeadings(directory);
|
|
2391
2391
|
|
|
2392
2392
|
// Step 9: Check broken links and missing resources (no auto-fix)
|
|
2393
|
-
console.log(chalk.yellow('\n🔗
|
|
2393
|
+
console.log(chalk.yellow('\n🔗 Bước 9: Kiểm tra link bên ngoài...'));
|
|
2394
2394
|
results.brokenLinks = await this.checkBrokenLinks(directory);
|
|
2395
2395
|
|
|
2396
|
-
console.log(chalk.yellow('\n📁
|
|
2396
|
+
console.log(chalk.yellow('\n📁 Bước 9b: Kiểm tra tài nguyên thiếu...'));
|
|
2397
2397
|
results.missingResources = await this.check404Resources(directory);
|
|
2398
2398
|
|
|
2399
2399
|
// Step 10: Cleanup duplicate roles
|
|
2400
|
-
console.log(chalk.yellow('\n🧹
|
|
2400
|
+
console.log(chalk.yellow('\n🧹 Bước 10: Dọn dẹp role trùng lặp...'));
|
|
2401
2401
|
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
2402
2402
|
|
|
2403
2403
|
// Summary
|
|
@@ -2433,27 +2433,27 @@ class AccessibilityFixer {
|
|
|
2433
2433
|
results.landmarks.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
2434
2434
|
results.cleanup.filter(r => r.status === 'fixed').length;
|
|
2435
2435
|
|
|
2436
|
-
console.log(chalk.green('\n🎉
|
|
2437
|
-
console.log(chalk.blue('📊
|
|
2438
|
-
console.log(chalk.white(`
|
|
2439
|
-
console.log(chalk.green(`
|
|
2440
|
-
console.log(chalk.yellow(`
|
|
2436
|
+
console.log(chalk.green('\n🎉 Hoàn tất tất cả các sửa lỗi accessibility!'));
|
|
2437
|
+
console.log(chalk.blue('📊 Tóm tắt cuối cùng:'));
|
|
2438
|
+
console.log(chalk.white(` Tổng file đã quét: ${totalFiles}`));
|
|
2439
|
+
console.log(chalk.green(` File đã sửa: ${totalFixed}`));
|
|
2440
|
+
console.log(chalk.yellow(` Tổng vấn đề đã giải quyết: ${totalIssues}`));
|
|
2441
2441
|
|
|
2442
2442
|
if (this.config.dryRun) {
|
|
2443
|
-
console.log(chalk.cyan('\n💡
|
|
2443
|
+
console.log(chalk.cyan('\n💡 Đây là chế độ xem trước. Sử dụng không có --dry-run để áp dụng thay đổi.'));
|
|
2444
2444
|
}
|
|
2445
2445
|
|
|
2446
2446
|
return results;
|
|
2447
2447
|
|
|
2448
2448
|
} catch (error) {
|
|
2449
|
-
console.error(chalk.red('❌
|
|
2449
|
+
console.error(chalk.red('❌ Lỗi trong quá trình sửa toàn diện:'), error.message);
|
|
2450
2450
|
throw error;
|
|
2451
2451
|
}
|
|
2452
2452
|
}
|
|
2453
2453
|
|
|
2454
2454
|
// Fix form labels
|
|
2455
2455
|
async fixFormLabels(directory = '.') {
|
|
2456
|
-
console.log(chalk.blue('📋
|
|
2456
|
+
console.log(chalk.blue('📋 Đang sửa nhãn form...'));
|
|
2457
2457
|
|
|
2458
2458
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2459
2459
|
const results = [];
|
|
@@ -2494,7 +2494,7 @@ class AccessibilityFixer {
|
|
|
2494
2494
|
}
|
|
2495
2495
|
}
|
|
2496
2496
|
|
|
2497
|
-
console.log(chalk.blue(`\n📊
|
|
2497
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề nhãn form trong ${results.length} file`));
|
|
2498
2498
|
return results;
|
|
2499
2499
|
}
|
|
2500
2500
|
|
|
@@ -2608,7 +2608,7 @@ class AccessibilityFixer {
|
|
|
2608
2608
|
|
|
2609
2609
|
// Fix button names
|
|
2610
2610
|
async fixButtonNames(directory = '.') {
|
|
2611
|
-
console.log(chalk.blue('🔘
|
|
2611
|
+
console.log(chalk.blue('🔘 Đang sửa tên button...'));
|
|
2612
2612
|
|
|
2613
2613
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2614
2614
|
const results = [];
|
|
@@ -2649,7 +2649,7 @@ class AccessibilityFixer {
|
|
|
2649
2649
|
}
|
|
2650
2650
|
}
|
|
2651
2651
|
|
|
2652
|
-
console.log(chalk.blue(`\n📊
|
|
2652
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề tên button trong ${results.length} file`));
|
|
2653
2653
|
return results;
|
|
2654
2654
|
}
|
|
2655
2655
|
|
|
@@ -2735,7 +2735,7 @@ class AccessibilityFixer {
|
|
|
2735
2735
|
|
|
2736
2736
|
// Fix link names
|
|
2737
2737
|
async fixLinkNames(directory = '.') {
|
|
2738
|
-
console.log(chalk.blue('🔗
|
|
2738
|
+
console.log(chalk.blue('🔗 Đang sửa tên link...'));
|
|
2739
2739
|
|
|
2740
2740
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2741
2741
|
const results = [];
|
|
@@ -2776,7 +2776,7 @@ class AccessibilityFixer {
|
|
|
2776
2776
|
}
|
|
2777
2777
|
}
|
|
2778
2778
|
|
|
2779
|
-
console.log(chalk.blue(`\n
|
|
2779
|
+
console.log(chalk.blue(`\n�� Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề tên link trong ${results.length} file`));
|
|
2780
2780
|
return results;
|
|
2781
2781
|
}
|
|
2782
2782
|
|
|
@@ -2856,7 +2856,7 @@ class AccessibilityFixer {
|
|
|
2856
2856
|
|
|
2857
2857
|
// Fix landmarks
|
|
2858
2858
|
async fixLandmarks(directory = '.') {
|
|
2859
|
-
console.log(chalk.blue('🏛️
|
|
2859
|
+
console.log(chalk.blue('🏛️ Đang sửa landmark...'));
|
|
2860
2860
|
|
|
2861
2861
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2862
2862
|
const results = [];
|
|
@@ -2897,7 +2897,7 @@ class AccessibilityFixer {
|
|
|
2897
2897
|
}
|
|
2898
2898
|
}
|
|
2899
2899
|
|
|
2900
|
-
console.log(chalk.blue(`\n📊
|
|
2900
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề landmark trong ${results.length} file`));
|
|
2901
2901
|
return results;
|
|
2902
2902
|
}
|
|
2903
2903
|
|
|
@@ -2986,7 +2986,7 @@ class AccessibilityFixer {
|
|
|
2986
2986
|
|
|
2987
2987
|
// Check for broken external links only
|
|
2988
2988
|
async checkBrokenLinks(directory = '.') {
|
|
2989
|
-
console.log(chalk.blue('🔗
|
|
2989
|
+
console.log(chalk.blue('🔗 Đang kiểm tra link bên ngoài bị lỗi...'));
|
|
2990
2990
|
|
|
2991
2991
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2992
2992
|
const results = [];
|
|
@@ -3013,7 +3013,7 @@ class AccessibilityFixer {
|
|
|
3013
3013
|
}
|
|
3014
3014
|
}
|
|
3015
3015
|
|
|
3016
|
-
console.log(chalk.blue(`\n📊
|
|
3016
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã phân tích link bên ngoài trong ${results.length} file`));
|
|
3017
3017
|
console.log(chalk.gray('💡 Broken link issues require manual review and cannot be auto-fixed'));
|
|
3018
3018
|
return results;
|
|
3019
3019
|
}
|
|
@@ -3047,11 +3047,191 @@ class AccessibilityFixer {
|
|
|
3047
3047
|
}
|
|
3048
3048
|
}
|
|
3049
3049
|
|
|
3050
|
-
console.log(chalk.blue(`\n📊
|
|
3050
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã phân tích tài nguyên cục bộ trong ${results.length} file`));
|
|
3051
3051
|
console.log(chalk.gray('💡 Missing resource issues require manual review and cannot be auto-fixed'));
|
|
3052
3052
|
return results;
|
|
3053
3053
|
}
|
|
3054
3054
|
|
|
3055
|
+
// Check Google Tag Manager installation
|
|
3056
|
+
async checkGoogleTagManager(directory = '.') {
|
|
3057
|
+
console.log(chalk.blue('🏷️ Đang kiểm tra cài đặt Google Tag Manager (GTM)...'));
|
|
3058
|
+
|
|
3059
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3060
|
+
const results = [];
|
|
3061
|
+
|
|
3062
|
+
for (const file of htmlFiles) {
|
|
3063
|
+
try {
|
|
3064
|
+
const content = await fs.readFile(file, 'utf8');
|
|
3065
|
+
const gtmAnalysis = this.analyzeGTMInstallation(content, file);
|
|
3066
|
+
|
|
3067
|
+
if (gtmAnalysis.hasGTM || gtmAnalysis.issues.length > 0) {
|
|
3068
|
+
console.log(chalk.cyan(`\n📁 ${file}:`));
|
|
3069
|
+
|
|
3070
|
+
if (gtmAnalysis.hasGTM) {
|
|
3071
|
+
console.log(chalk.green(` ✅ GTM Container ID: ${gtmAnalysis.containerId}`));
|
|
3072
|
+
|
|
3073
|
+
if (gtmAnalysis.headScriptCorrect) {
|
|
3074
|
+
console.log(chalk.green(` ✅ Script trong head: Đã đặt đúng vị trí trước </head>`));
|
|
3075
|
+
} else if (gtmAnalysis.hasHeadScript) {
|
|
3076
|
+
console.log(chalk.yellow(` ⚠️ Script trong head: Tìm thấy nhưng có thể chưa đặt ở vị trí tối ưu`));
|
|
3077
|
+
} else {
|
|
3078
|
+
console.log(chalk.red(` ❌ Script trong head: Thiếu trong phần <head>`));
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
if (gtmAnalysis.bodyNoscriptCorrect) {
|
|
3082
|
+
console.log(chalk.green(` ✅ Noscript trong body: Đã đặt đúng vị trí sau <body>`));
|
|
3083
|
+
} else if (gtmAnalysis.hasBodyNoscript) {
|
|
3084
|
+
console.log(chalk.yellow(` ⚠️ Noscript trong body: Tìm thấy nhưng có thể chưa đặt ở vị trí tối ưu`));
|
|
3085
|
+
} else {
|
|
3086
|
+
console.log(chalk.red(` ❌ Noscript trong body: Thiếu sau thẻ <body>`));
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
gtmAnalysis.issues.forEach(issue => {
|
|
3091
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
3092
|
+
if (issue.suggestion) {
|
|
3093
|
+
console.log(chalk.gray(` 💡 ${issue.suggestion}`));
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
results.push({
|
|
3099
|
+
file,
|
|
3100
|
+
status: 'analyzed',
|
|
3101
|
+
gtmAnalysis
|
|
3102
|
+
});
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
console.error(chalk.red(`❌ Lỗi khi xử lý ${file}: ${error.message}`));
|
|
3105
|
+
results.push({ file, status: 'error', error: error.message });
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
const filesWithGTM = results.filter(r => r.gtmAnalysis?.hasGTM).length;
|
|
3110
|
+
const filesWithIssues = results.filter(r => r.gtmAnalysis?.issues?.length > 0).length;
|
|
3111
|
+
|
|
3112
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã phân tích ${results.length} file`));
|
|
3113
|
+
console.log(chalk.green(` ✅ File có GTM: ${filesWithGTM}`));
|
|
3114
|
+
if (filesWithIssues > 0) {
|
|
3115
|
+
console.log(chalk.yellow(` ⚠️ File có vấn đề về GTM: ${filesWithIssues}`));
|
|
3116
|
+
}
|
|
3117
|
+
console.log(chalk.gray('💡 GTM cần có cả <script> trong <head> và <noscript> sau <body>'));
|
|
3118
|
+
|
|
3119
|
+
return results;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
analyzeGTMInstallation(content, filePath) {
|
|
3123
|
+
const result = {
|
|
3124
|
+
hasGTM: false,
|
|
3125
|
+
containerId: null,
|
|
3126
|
+
hasHeadScript: false,
|
|
3127
|
+
hasBodyNoscript: false,
|
|
3128
|
+
headScriptCorrect: false,
|
|
3129
|
+
bodyNoscriptCorrect: false,
|
|
3130
|
+
issues: []
|
|
3131
|
+
};
|
|
3132
|
+
|
|
3133
|
+
// Pattern to detect GTM container ID - more flexible to handle line breaks and spaces
|
|
3134
|
+
const gtmScriptPattern = /googletagmanager\.com\/gtm\.js[^'"]*['"]?\s*\+?\s*i\s*\+?\s*dl[^)]*\)\s*[,;]\s*['"]([^'"]*)['"]\s*,\s*['"]script['"]\s*,\s*['"]dataLayer['"]\s*,\s*['"]?(GTM-[A-Z0-9]+)['"]?\)/i;
|
|
3135
|
+
const gtmContainerPattern = /GTM-[A-Z0-9]+/g; // Simpler pattern to find any GTM container ID
|
|
3136
|
+
const gtmNoscriptPattern = /googletagmanager\.com\/ns\.html\?id=(GTM-[A-Z0-9]+)/i;
|
|
3137
|
+
|
|
3138
|
+
// Check for GTM script in head - look for container ID in the script section
|
|
3139
|
+
const scriptMatch = content.match(gtmScriptPattern);
|
|
3140
|
+
let containerId = null;
|
|
3141
|
+
|
|
3142
|
+
// Extract head content
|
|
3143
|
+
const headContent = content.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
3144
|
+
|
|
3145
|
+
if (headContent) {
|
|
3146
|
+
// Look for GTM container ID in head
|
|
3147
|
+
const headContainerMatches = headContent[1].match(gtmContainerPattern);
|
|
3148
|
+
if (headContainerMatches) {
|
|
3149
|
+
// Filter to only GTM container IDs (not other codes)
|
|
3150
|
+
const gtmIds = headContainerMatches.filter(id => id.startsWith('GTM-'));
|
|
3151
|
+
if (gtmIds.length > 0) {
|
|
3152
|
+
containerId = gtmIds[0]; // Use first found GTM ID
|
|
3153
|
+
result.hasGTM = true;
|
|
3154
|
+
result.containerId = containerId;
|
|
3155
|
+
result.hasHeadScript = true;
|
|
3156
|
+
|
|
3157
|
+
// Check if googletagmanager.com is mentioned in head (confirms it's GTM script)
|
|
3158
|
+
if (headContent[1].includes('googletagmanager.com')) {
|
|
3159
|
+
result.headScriptCorrect = true;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
if (!result.hasHeadScript && scriptMatch) {
|
|
3166
|
+
// Fallback: script pattern matched but not in head
|
|
3167
|
+
result.hasGTM = true;
|
|
3168
|
+
result.containerId = scriptMatch[2] || scriptMatch[1];
|
|
3169
|
+
result.hasHeadScript = true;
|
|
3170
|
+
result.issues.push({
|
|
3171
|
+
type: '⚠️ Vị trí GTM Script',
|
|
3172
|
+
description: 'Tìm thấy GTM script nhưng không nằm trong phần <head>',
|
|
3173
|
+
suggestion: 'Di chuyển GTM script vào phần <head>, tốt nhất là đặt trước thẻ đóng </head>'
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// Check for GTM noscript in body
|
|
3178
|
+
const noscriptMatch = content.match(gtmNoscriptPattern);
|
|
3179
|
+
if (noscriptMatch) {
|
|
3180
|
+
if (!result.hasGTM) {
|
|
3181
|
+
result.hasGTM = true;
|
|
3182
|
+
result.containerId = noscriptMatch[1];
|
|
3183
|
+
}
|
|
3184
|
+
result.hasBodyNoscript = true;
|
|
3185
|
+
|
|
3186
|
+
// Check if noscript is right after <body> tag
|
|
3187
|
+
const bodyStartPattern = /<body[^>]*>([\s\S]{0,500})/i;
|
|
3188
|
+
const bodyStartMatch = content.match(bodyStartPattern);
|
|
3189
|
+
if (bodyStartMatch && bodyStartMatch[1].includes(noscriptMatch[0])) {
|
|
3190
|
+
// Check if it's within first 200 chars after <body> (good practice)
|
|
3191
|
+
const bodyStartPart = bodyStartMatch[1].slice(0, 200);
|
|
3192
|
+
if (bodyStartPart.includes(noscriptMatch[0])) {
|
|
3193
|
+
result.bodyNoscriptCorrect = true;
|
|
3194
|
+
}
|
|
3195
|
+
} else {
|
|
3196
|
+
result.issues.push({
|
|
3197
|
+
type: '⚠️ Vị trí GTM Noscript',
|
|
3198
|
+
description: 'Tìm thấy GTM noscript nhưng không nằm ngay sau thẻ <body>',
|
|
3199
|
+
suggestion: 'Di chuyển GTM noscript về ngay sau thẻ mở <body>'
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Check for mismatched container IDs
|
|
3205
|
+
if (containerId && noscriptMatch && containerId !== noscriptMatch[1]) {
|
|
3206
|
+
result.issues.push({
|
|
3207
|
+
type: '❌ Container ID không khớp',
|
|
3208
|
+
description: `Script sử dụng ${containerId} nhưng noscript sử dụng ${noscriptMatch[1]}`,
|
|
3209
|
+
suggestion: 'Đảm bảo cả hai đoạn mã GTM sử dụng cùng một container ID'
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// Check for incomplete installation
|
|
3214
|
+
if (result.hasGTM) {
|
|
3215
|
+
if (!result.hasHeadScript) {
|
|
3216
|
+
result.issues.push({
|
|
3217
|
+
type: '❌ Thiếu GTM Script',
|
|
3218
|
+
description: 'Tìm thấy GTM noscript nhưng thiếu script chính trong <head>',
|
|
3219
|
+
suggestion: 'Thêm đoạn mã GTM script vào phần <head> trước thẻ </head>'
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
if (!result.hasBodyNoscript) {
|
|
3224
|
+
result.issues.push({
|
|
3225
|
+
type: '❌ Thiếu GTM Noscript',
|
|
3226
|
+
description: 'Tìm thấy GTM script nhưng thiếu noscript dự phòng trong <body>',
|
|
3227
|
+
suggestion: 'Thêm đoạn mã GTM noscript ngay sau thẻ mở <body>'
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
return result;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3055
3235
|
async analyzeBrokenLinks(content, filePath, mode = 'all') {
|
|
3056
3236
|
const issues = [];
|
|
3057
3237
|
const path = require('path');
|
|
@@ -3279,7 +3459,7 @@ class AccessibilityFixer {
|
|
|
3279
3459
|
|
|
3280
3460
|
// Analyze headings (no auto-fix, only suggestions)
|
|
3281
3461
|
async analyzeHeadings(directory = '.') {
|
|
3282
|
-
console.log(chalk.blue('📑
|
|
3462
|
+
console.log(chalk.blue('📑 Đang phân tích cấu trúc heading...'));
|
|
3283
3463
|
|
|
3284
3464
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3285
3465
|
const results = [];
|
|
@@ -3306,7 +3486,7 @@ class AccessibilityFixer {
|
|
|
3306
3486
|
}
|
|
3307
3487
|
}
|
|
3308
3488
|
|
|
3309
|
-
console.log(chalk.blue(`\n📊
|
|
3489
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã phân tích cấu trúc heading trong ${results.length} file`));
|
|
3310
3490
|
console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
|
|
3311
3491
|
return results;
|
|
3312
3492
|
}
|
|
@@ -3353,7 +3533,7 @@ class AccessibilityFixer {
|
|
|
3353
3533
|
}
|
|
3354
3534
|
}
|
|
3355
3535
|
|
|
3356
|
-
console.log(chalk.blue(`\n📊
|
|
3536
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề nhãn form trong ${results.length} file`));
|
|
3357
3537
|
return results;
|
|
3358
3538
|
}
|
|
3359
3539
|
|
|
@@ -3760,55 +3940,55 @@ class AccessibilityFixer {
|
|
|
3760
3940
|
|
|
3761
3941
|
try {
|
|
3762
3942
|
// Step 1: HTML lang attributes
|
|
3763
|
-
console.log(chalk.blue('📝
|
|
3943
|
+
console.log(chalk.blue('📝 Bước 1: Thuộc tính HTML lang...'));
|
|
3764
3944
|
const langResults = await this.fixHtmlLang(directory);
|
|
3765
3945
|
const langFixed = langResults.filter(r => r.status === 'fixed').length;
|
|
3766
3946
|
results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
|
|
3767
3947
|
|
|
3768
3948
|
// Step 2: Alt attributes
|
|
3769
|
-
console.log(chalk.blue('🖼️
|
|
3949
|
+
console.log(chalk.blue('🖼️ Bước 2: Thuộc tính alt...'));
|
|
3770
3950
|
const altResults = await this.fixEmptyAltAttributes(directory);
|
|
3771
3951
|
const altFixed = altResults.filter(r => r.status === 'fixed').length;
|
|
3772
3952
|
const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3773
3953
|
results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
|
|
3774
3954
|
|
|
3775
3955
|
// Step 3: Role attributes
|
|
3776
|
-
console.log(chalk.blue('🎭
|
|
3956
|
+
console.log(chalk.blue('🎭 Bước 3: Thuộc tính role...'));
|
|
3777
3957
|
const roleResults = await this.fixRoleAttributes(directory);
|
|
3778
3958
|
const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
|
|
3779
3959
|
const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3780
3960
|
results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
|
|
3781
3961
|
|
|
3782
3962
|
// Step 4: Form labels
|
|
3783
|
-
console.log(chalk.blue('📋
|
|
3963
|
+
console.log(chalk.blue('📋 Bước 4: Nhãn form...'));
|
|
3784
3964
|
const formResults = await this.fixFormLabels(directory);
|
|
3785
3965
|
const formFixed = formResults.filter(r => r.status === 'fixed').length;
|
|
3786
3966
|
const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3787
3967
|
results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
|
|
3788
3968
|
|
|
3789
3969
|
// Step 5: Button names
|
|
3790
|
-
console.log(chalk.blue('🔘
|
|
3970
|
+
console.log(chalk.blue('🔘 Bước 5: Tên button...'));
|
|
3791
3971
|
const buttonResults = await this.fixButtonNames(directory);
|
|
3792
3972
|
const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
|
|
3793
3973
|
const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3794
3974
|
results.steps.push({ step: 5, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
|
|
3795
3975
|
|
|
3796
3976
|
// Step 6: Link names
|
|
3797
|
-
console.log(chalk.blue('🔗
|
|
3977
|
+
console.log(chalk.blue('🔗 Bước 6: Tên link...'));
|
|
3798
3978
|
const linkResults = await this.fixLinkNames(directory);
|
|
3799
3979
|
const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
|
|
3800
3980
|
const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3801
3981
|
results.steps.push({ step: 6, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
|
|
3802
3982
|
|
|
3803
3983
|
// Step 7: Landmarks
|
|
3804
|
-
console.log(chalk.blue('🏛️
|
|
3984
|
+
console.log(chalk.blue('🏛️ Bước 7: Landmark...'));
|
|
3805
3985
|
const landmarkResults = await this.fixLandmarks(directory);
|
|
3806
3986
|
const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
|
|
3807
3987
|
const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3808
3988
|
results.steps.push({ step: 7, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
|
|
3809
3989
|
|
|
3810
3990
|
// Step 8: Heading analysis
|
|
3811
|
-
console.log(chalk.blue('📑
|
|
3991
|
+
console.log(chalk.blue('📑 Bước 8: Phân tích heading...'));
|
|
3812
3992
|
const headingResults = await this.analyzeHeadings(directory);
|
|
3813
3993
|
const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3814
3994
|
results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
@@ -3820,7 +4000,7 @@ class AccessibilityFixer {
|
|
|
3820
4000
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3821
4001
|
results.steps.push({ step: '9a', name: 'External links check', issues: totalBrokenLinks });
|
|
3822
4002
|
|
|
3823
|
-
console.log(chalk.blue('📁
|
|
4003
|
+
console.log(chalk.blue('📁 Bước 9b: Kiểm tra tài nguyên thiếu...'));
|
|
3824
4004
|
const missingResourcesResults = await this.check404Resources(directory);
|
|
3825
4005
|
const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3826
4006
|
results.steps.push({ step: '9b', name: 'Missing resources check', issues: totalMissingResources });
|
|
@@ -3828,7 +4008,7 @@ class AccessibilityFixer {
|
|
|
3828
4008
|
console.log(chalk.gray('💡 Link and resource issues require manual review and cannot be auto-fixed'));
|
|
3829
4009
|
|
|
3830
4010
|
// Step 10: Cleanup duplicate roles
|
|
3831
|
-
console.log(chalk.blue('🧹
|
|
4011
|
+
console.log(chalk.blue('🧹 Bước 10: Dọn dẹp role trùng lặp...'));
|
|
3832
4012
|
const cleanupResults = await this.cleanupDuplicateRoles(directory);
|
|
3833
4013
|
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
3834
4014
|
results.steps.push({ step: 10, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
|
|
@@ -3874,7 +4054,7 @@ class AccessibilityFixer {
|
|
|
3874
4054
|
}
|
|
3875
4055
|
|
|
3876
4056
|
async fixButtonNames(directory = '.') {
|
|
3877
|
-
console.log(chalk.blue('🔘
|
|
4057
|
+
console.log(chalk.blue('🔘 Đang sửa tên button...'));
|
|
3878
4058
|
|
|
3879
4059
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3880
4060
|
const results = [];
|
|
@@ -3915,7 +4095,7 @@ class AccessibilityFixer {
|
|
|
3915
4095
|
}
|
|
3916
4096
|
}
|
|
3917
4097
|
|
|
3918
|
-
console.log(chalk.blue(`\n📊
|
|
4098
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề tên button trong ${results.length} file`));
|
|
3919
4099
|
return results;
|
|
3920
4100
|
}
|
|
3921
4101
|
|
|
@@ -3991,7 +4171,7 @@ class AccessibilityFixer {
|
|
|
3991
4171
|
}
|
|
3992
4172
|
|
|
3993
4173
|
async fixLinkNames(directory = '.') {
|
|
3994
|
-
console.log(chalk.blue('🔗
|
|
4174
|
+
console.log(chalk.blue('🔗 Đang sửa tên link...'));
|
|
3995
4175
|
|
|
3996
4176
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3997
4177
|
const results = [];
|
|
@@ -4032,7 +4212,7 @@ class AccessibilityFixer {
|
|
|
4032
4212
|
}
|
|
4033
4213
|
}
|
|
4034
4214
|
|
|
4035
|
-
console.log(chalk.blue(`\n
|
|
4215
|
+
console.log(chalk.blue(`\n�� Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề tên link trong ${results.length} file`));
|
|
4036
4216
|
return results;
|
|
4037
4217
|
}
|
|
4038
4218
|
|
|
@@ -4108,7 +4288,7 @@ class AccessibilityFixer {
|
|
|
4108
4288
|
}
|
|
4109
4289
|
|
|
4110
4290
|
async fixLandmarks(directory = '.') {
|
|
4111
|
-
console.log(chalk.blue('🏛️
|
|
4291
|
+
console.log(chalk.blue('🏛️ Đang sửa landmark...'));
|
|
4112
4292
|
|
|
4113
4293
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4114
4294
|
const results = [];
|
|
@@ -4134,7 +4314,7 @@ class AccessibilityFixer {
|
|
|
4134
4314
|
}
|
|
4135
4315
|
}
|
|
4136
4316
|
|
|
4137
|
-
console.log(chalk.blue(`\n📊
|
|
4317
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề landmark trong ${results.length} file`));
|
|
4138
4318
|
return results;
|
|
4139
4319
|
}
|
|
4140
4320
|
|
|
@@ -4158,7 +4338,7 @@ class AccessibilityFixer {
|
|
|
4158
4338
|
}
|
|
4159
4339
|
|
|
4160
4340
|
async analyzeHeadings(directory = '.') {
|
|
4161
|
-
console.log(chalk.blue('📑
|
|
4341
|
+
console.log(chalk.blue('📑 Đang phân tích cấu trúc heading...'));
|
|
4162
4342
|
|
|
4163
4343
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4164
4344
|
const results = [];
|
|
@@ -4187,7 +4367,7 @@ class AccessibilityFixer {
|
|
|
4187
4367
|
}
|
|
4188
4368
|
}
|
|
4189
4369
|
|
|
4190
|
-
console.log(chalk.blue(`\n📊
|
|
4370
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã phân tích cấu trúc heading trong ${results.length} file`));
|
|
4191
4371
|
console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
|
|
4192
4372
|
return results;
|
|
4193
4373
|
}
|
|
@@ -4195,7 +4375,7 @@ class AccessibilityFixer {
|
|
|
4195
4375
|
|
|
4196
4376
|
|
|
4197
4377
|
async cleanupDuplicateRoles(directory = '.') {
|
|
4198
|
-
console.log(chalk.blue('🧹
|
|
4378
|
+
console.log(chalk.blue('🧹 Đang dọn dẹp các thuộc tính role trùng lặp...'));
|
|
4199
4379
|
|
|
4200
4380
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4201
4381
|
const results = [];
|
|
@@ -4227,7 +4407,7 @@ class AccessibilityFixer {
|
|
|
4227
4407
|
}
|
|
4228
4408
|
}
|
|
4229
4409
|
|
|
4230
|
-
console.log(chalk.blue(`\n📊
|
|
4410
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Đã dọn dẹp role trùng lặp trong ${totalIssuesFound} file`));
|
|
4231
4411
|
return results;
|
|
4232
4412
|
}
|
|
4233
4413
|
|
|
@@ -4241,7 +4421,7 @@ class AccessibilityFixer {
|
|
|
4241
4421
|
}
|
|
4242
4422
|
|
|
4243
4423
|
async fixNestedInteractiveControls(directory = '.') {
|
|
4244
|
-
console.log(chalk.blue('🎯
|
|
4424
|
+
console.log(chalk.blue('🎯 Đang sửa các control tương tác lồng nhau...'));
|
|
4245
4425
|
|
|
4246
4426
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4247
4427
|
const results = [];
|
|
@@ -4285,7 +4465,7 @@ class AccessibilityFixer {
|
|
|
4285
4465
|
}
|
|
4286
4466
|
}
|
|
4287
4467
|
|
|
4288
|
-
console.log(chalk.blue(`\n📊
|
|
4468
|
+
console.log(chalk.blue(`\n📊 Tóm tắt: Tìm thấy ${totalIssuesFound} vấn đề control tương tác lồng nhau trong ${results.length} file`));
|
|
4289
4469
|
return results;
|
|
4290
4470
|
}
|
|
4291
4471
|
|
|
@@ -4486,81 +4666,81 @@ class AccessibilityFixer {
|
|
|
4486
4666
|
|
|
4487
4667
|
try {
|
|
4488
4668
|
// Step 1: HTML lang attributes
|
|
4489
|
-
console.log(chalk.blue('📝
|
|
4669
|
+
console.log(chalk.blue('📝 Bước 1: Thuộc tính HTML lang...'));
|
|
4490
4670
|
const langResults = await this.fixHtmlLang(directory);
|
|
4491
4671
|
const langFixed = langResults.filter(r => r.status === 'fixed').length;
|
|
4492
4672
|
results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
|
|
4493
4673
|
|
|
4494
4674
|
// Step 2: Alt attributes
|
|
4495
|
-
console.log(chalk.blue('🖼️
|
|
4675
|
+
console.log(chalk.blue('🖼️ Bước 2: Thuộc tính alt...'));
|
|
4496
4676
|
const altResults = await this.fixEmptyAltAttributes(directory);
|
|
4497
4677
|
const altFixed = altResults.filter(r => r.status === 'fixed').length;
|
|
4498
4678
|
const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4499
4679
|
results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
|
|
4500
4680
|
|
|
4501
4681
|
// Step 3: Role attributes
|
|
4502
|
-
console.log(chalk.blue('🎭
|
|
4682
|
+
console.log(chalk.blue('🎭 Bước 3: Thuộc tính role...'));
|
|
4503
4683
|
const roleResults = await this.fixRoleAttributes(directory);
|
|
4504
4684
|
const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
|
|
4505
4685
|
const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4506
4686
|
results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
|
|
4507
4687
|
|
|
4508
4688
|
// Step 4: Aria-label attributes
|
|
4509
|
-
console.log(chalk.blue('🏷️
|
|
4689
|
+
console.log(chalk.blue('🏷️ Bước 4: Thuộc tính aria-label...'));
|
|
4510
4690
|
const ariaResults = await this.fixAriaLabels(directory);
|
|
4511
4691
|
const ariaFixed = ariaResults.filter(r => r.status === 'processed' && r.changes > 0).length;
|
|
4512
4692
|
const totalAriaIssues = ariaResults.reduce((sum, r) => sum + (r.changes || 0), 0);
|
|
4513
4693
|
results.steps.push({ step: 4, name: 'Aria-label attributes', fixed: ariaFixed, issues: totalAriaIssues });
|
|
4514
4694
|
|
|
4515
4695
|
// Step 5: Form labels
|
|
4516
|
-
console.log(chalk.blue('📋
|
|
4696
|
+
console.log(chalk.blue('📋 Bước 5: Nhãn form...'));
|
|
4517
4697
|
const formResults = await this.fixFormLabels(directory);
|
|
4518
4698
|
const formFixed = formResults.filter(r => r.status === 'fixed').length;
|
|
4519
4699
|
const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4520
4700
|
results.steps.push({ step: 5, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
|
|
4521
4701
|
|
|
4522
4702
|
// Step 6: Nested interactive controls (NEW!)
|
|
4523
|
-
console.log(chalk.blue('🎯
|
|
4703
|
+
console.log(chalk.blue('🎯 Bước 6: Các control tương tác lồng nhau...'));
|
|
4524
4704
|
const nestedResults = await this.fixNestedInteractiveControls(directory);
|
|
4525
4705
|
const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
|
|
4526
4706
|
const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4527
4707
|
results.steps.push({ step: 6, name: 'Nested interactive controls', fixed: nestedFixed, issues: totalNestedIssues });
|
|
4528
4708
|
|
|
4529
4709
|
// Step 7: Button names
|
|
4530
|
-
console.log(chalk.blue('🔘
|
|
4710
|
+
console.log(chalk.blue('🔘 Bước 7: Tên button...'));
|
|
4531
4711
|
const buttonResults = await this.fixButtonNames(directory);
|
|
4532
4712
|
const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
|
|
4533
4713
|
const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4534
4714
|
results.steps.push({ step: 7, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
|
|
4535
4715
|
|
|
4536
4716
|
// Step 8: Link names
|
|
4537
|
-
console.log(chalk.blue('🔗
|
|
4717
|
+
console.log(chalk.blue('🔗 Bước 8: Tên link...'));
|
|
4538
4718
|
const linkResults = await this.fixLinkNames(directory);
|
|
4539
4719
|
const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
|
|
4540
4720
|
const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4541
4721
|
results.steps.push({ step: 8, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
|
|
4542
4722
|
|
|
4543
4723
|
// Step 9: Landmarks
|
|
4544
|
-
console.log(chalk.blue('🏛️
|
|
4724
|
+
console.log(chalk.blue('🏛️ Bước 9: Landmark...'));
|
|
4545
4725
|
const landmarkResults = await this.fixLandmarks(directory);
|
|
4546
4726
|
const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
|
|
4547
4727
|
const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4548
4728
|
results.steps.push({ step: 9, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
|
|
4549
4729
|
|
|
4550
4730
|
// Step 10: Heading analysis
|
|
4551
|
-
console.log(chalk.blue('📑
|
|
4731
|
+
console.log(chalk.blue('📑 Bước 10: Phân tích heading...'));
|
|
4552
4732
|
const headingResults = await this.analyzeHeadings(directory);
|
|
4553
4733
|
const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4554
4734
|
results.steps.push({ step: 10, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4555
4735
|
console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
|
|
4556
4736
|
|
|
4557
4737
|
// Step 11: Broken links and missing resources check
|
|
4558
|
-
console.log(chalk.blue('🔗
|
|
4738
|
+
console.log(chalk.blue('🔗 Bước 11a: Kiểm tra link bên ngoài...'));
|
|
4559
4739
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4560
4740
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4561
4741
|
results.steps.push({ step: '11a', name: 'External links check', issues: totalBrokenLinks });
|
|
4562
4742
|
|
|
4563
|
-
console.log(chalk.blue('
|
|
4743
|
+
console.log(chalk.blue('�� Bước 11b: Kiểm tra tài nguyên thiếu...'));
|
|
4564
4744
|
const missingResourcesResults = await this.check404Resources(directory);
|
|
4565
4745
|
const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4566
4746
|
results.steps.push({ step: '11b', name: 'Missing resources check', issues: totalMissingResources });
|
|
@@ -4568,7 +4748,7 @@ class AccessibilityFixer {
|
|
|
4568
4748
|
console.log(chalk.gray('💡 Link and resource issues require manual review and cannot be auto-fixed'));
|
|
4569
4749
|
|
|
4570
4750
|
// Step 12: Cleanup duplicate roles
|
|
4571
|
-
console.log(chalk.blue('🧹
|
|
4751
|
+
console.log(chalk.blue('🧹 Bước 12: Dọn dẹp role trùng lặp...'));
|
|
4572
4752
|
const cleanupResults = await this.cleanupDuplicateRoles(directory);
|
|
4573
4753
|
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
4574
4754
|
results.steps.push({ step: 12, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
|
|
@@ -5710,9 +5890,9 @@ class AccessibilityFixer {
|
|
|
5710
5890
|
|
|
5711
5891
|
// Display results
|
|
5712
5892
|
if (unusedFiles.length === 0) {
|
|
5713
|
-
console.log(chalk.green('✅
|
|
5893
|
+
console.log(chalk.green('✅ Không tìm thấy file không sử dụng! Tất cả file đều được tham chiếu đúng cách.'));
|
|
5714
5894
|
} else {
|
|
5715
|
-
console.log(chalk.yellow(`\n📋
|
|
5895
|
+
console.log(chalk.yellow(`\n📋 Tìm thấy ${unusedFiles.length} file có thể không sử dụng:`));
|
|
5716
5896
|
|
|
5717
5897
|
unusedFiles.forEach((file, index) => {
|
|
5718
5898
|
const icon = this.getFileIcon(file.type);
|
|
@@ -5720,12 +5900,12 @@ class AccessibilityFixer {
|
|
|
5720
5900
|
});
|
|
5721
5901
|
|
|
5722
5902
|
const totalSize = unusedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
5723
|
-
console.log(chalk.blue(`\n📊
|
|
5724
|
-
console.log(chalk.gray('💡
|
|
5903
|
+
console.log(chalk.blue(`\n📊 Tổng kích thước file không sử dụng: ${this.formatFileSize(totalSize)}`));
|
|
5904
|
+
console.log(chalk.gray('💡 Xem xét kỹ các file này trước khi xóa - một số có thể được sử dụng động hoặc cần thiết cho deployment'));
|
|
5725
5905
|
}
|
|
5726
5906
|
|
|
5727
5907
|
const endTime = Date.now();
|
|
5728
|
-
console.log(chalk.gray(`⏱️
|
|
5908
|
+
console.log(chalk.gray(`⏱️ Phân tích hoàn tất trong ${endTime - startTime}ms`));
|
|
5729
5909
|
|
|
5730
5910
|
return {
|
|
5731
5911
|
unusedFiles: unusedFiles,
|
|
@@ -5841,7 +6021,7 @@ class AccessibilityFixer {
|
|
|
5841
6021
|
return referencedFiles;
|
|
5842
6022
|
}
|
|
5843
6023
|
|
|
5844
|
-
// Find HTML
|
|
6024
|
+
// Find HTML, CSS/SCSS, JS, and other source files to scan for references
|
|
5845
6025
|
async findSourceFiles(directory) {
|
|
5846
6026
|
const sourceFiles = [];
|
|
5847
6027
|
|
|
@@ -5858,9 +6038,11 @@ class AccessibilityFixer {
|
|
|
5858
6038
|
await walk(filePath);
|
|
5859
6039
|
}
|
|
5860
6040
|
} else {
|
|
5861
|
-
// Include HTML
|
|
6041
|
+
// Include HTML, CSS/SCSS, JavaScript, TypeScript, Vue, PHP, JSON, XML, SVG
|
|
5862
6042
|
const ext = path.extname(file).toLowerCase();
|
|
5863
|
-
if (['.html', '.htm', '.css', '.scss', '.sass'
|
|
6043
|
+
if (['.html', '.htm', '.css', '.scss', '.sass',
|
|
6044
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs',
|
|
6045
|
+
'.vue', '.php', '.json', '.xml', '.svg'].includes(ext)) {
|
|
5864
6046
|
sourceFiles.push(filePath);
|
|
5865
6047
|
}
|
|
5866
6048
|
}
|
|
@@ -5874,7 +6056,7 @@ class AccessibilityFixer {
|
|
|
5874
6056
|
return sourceFiles;
|
|
5875
6057
|
}
|
|
5876
6058
|
|
|
5877
|
-
// Enhanced reference extraction from HTML and
|
|
6059
|
+
// Enhanced reference extraction from HTML, CSS, JS, and other source files
|
|
5878
6060
|
extractAllReferences(content, baseDir, sourceFile) {
|
|
5879
6061
|
const references = [];
|
|
5880
6062
|
const ext = path.extname(sourceFile).toLowerCase();
|
|
@@ -5888,6 +6070,31 @@ class AccessibilityFixer {
|
|
|
5888
6070
|
// Extract from CSS/SCSS files - background images
|
|
5889
6071
|
const cssRefs = this.extractCssReferences(content, baseDir);
|
|
5890
6072
|
references.push(...cssRefs);
|
|
6073
|
+
|
|
6074
|
+
} else if (['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
|
|
6075
|
+
// Extract from JavaScript/TypeScript files - imports, requires, dynamic imports
|
|
6076
|
+
const jsRefs = this.extractJsReferences(content, baseDir);
|
|
6077
|
+
references.push(...jsRefs);
|
|
6078
|
+
|
|
6079
|
+
} else if (ext === '.vue') {
|
|
6080
|
+
// Extract from Vue files - template, script, and style sections
|
|
6081
|
+
const vueRefs = this.extractVueReferences(content, baseDir);
|
|
6082
|
+
references.push(...vueRefs);
|
|
6083
|
+
|
|
6084
|
+
} else if (ext === '.json') {
|
|
6085
|
+
// Extract from JSON files - config files, manifests
|
|
6086
|
+
const jsonRefs = this.extractJsonReferences(content, baseDir);
|
|
6087
|
+
references.push(...jsonRefs);
|
|
6088
|
+
|
|
6089
|
+
} else if (['.xml', '.svg'].includes(ext)) {
|
|
6090
|
+
// Extract from XML/SVG files - embedded references
|
|
6091
|
+
const xmlRefs = this.extractXmlReferences(content, baseDir);
|
|
6092
|
+
references.push(...xmlRefs);
|
|
6093
|
+
|
|
6094
|
+
} else if (ext === '.php') {
|
|
6095
|
+
// Extract from PHP files - mixed HTML/PHP content
|
|
6096
|
+
const phpRefs = this.extractPhpReferences(content, baseDir);
|
|
6097
|
+
references.push(...phpRefs);
|
|
5891
6098
|
}
|
|
5892
6099
|
|
|
5893
6100
|
return references;
|
|
@@ -5922,7 +6129,10 @@ class AccessibilityFixer {
|
|
|
5922
6129
|
// Iframe
|
|
5923
6130
|
/<iframe[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5924
6131
|
// Meta (for icons)
|
|
5925
|
-
/<meta[^>]*content\s*=\s*["']([^"']+\.(ico|png|jpg|jpeg|svg))["']/gi
|
|
6132
|
+
/<meta[^>]*content\s*=\s*["']([^"']+\.(ico|png|jpg|jpeg|svg))["']/gi,
|
|
6133
|
+
// Server Side Includes (SSI)
|
|
6134
|
+
/<!--#include\s+virtual\s*=\s*["']([^"']+)["']\s*-->/gi,
|
|
6135
|
+
/<!--#include\s+file\s*=\s*["']([^"']+)["']\s*-->/gi
|
|
5926
6136
|
];
|
|
5927
6137
|
|
|
5928
6138
|
for (const pattern of patterns) {
|
|
@@ -5990,16 +6200,22 @@ class AccessibilityFixer {
|
|
|
5990
6200
|
extractJsReferences(content, baseDir) {
|
|
5991
6201
|
const references = [];
|
|
5992
6202
|
|
|
5993
|
-
// JavaScript patterns for file references
|
|
6203
|
+
// JavaScript/TypeScript patterns for file references
|
|
5994
6204
|
const patterns = [
|
|
5995
|
-
//
|
|
5996
|
-
/require\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
5997
|
-
// import statements
|
|
6205
|
+
// ES6 import statements
|
|
5998
6206
|
/import\s+.*?from\s+["']([^"']+)["']/gi,
|
|
5999
|
-
//
|
|
6000
|
-
/
|
|
6207
|
+
// Dynamic imports
|
|
6208
|
+
/import\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6209
|
+
// CommonJS require
|
|
6210
|
+
/require\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6211
|
+
// fetch() API calls
|
|
6212
|
+
/fetch\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6001
6213
|
// XMLHttpRequest
|
|
6002
|
-
/\.open\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']
|
|
6214
|
+
/\.open\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']+)["']/gi,
|
|
6215
|
+
// String literals that look like paths
|
|
6216
|
+
/["']([^"']*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico))["']/gi,
|
|
6217
|
+
// Template literals with paths
|
|
6218
|
+
/`([^`]*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico))`/gi
|
|
6003
6219
|
];
|
|
6004
6220
|
|
|
6005
6221
|
for (const pattern of patterns) {
|
|
@@ -6007,13 +6223,76 @@ class AccessibilityFixer {
|
|
|
6007
6223
|
while ((match = pattern.exec(content)) !== null) {
|
|
6008
6224
|
const url = match[1];
|
|
6009
6225
|
if (this.isLocalFile(url)) {
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6226
|
+
this.addNormalizedUrl(references, url);
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
return references;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
extractVueReferences(content, baseDir) {
|
|
6235
|
+
const references = [];
|
|
6236
|
+
|
|
6237
|
+
// Extract from template section (HTML-like)
|
|
6238
|
+
const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
|
|
6239
|
+
if (templateMatch) {
|
|
6240
|
+
const htmlRefs = this.extractHtmlReferences(templateMatch[1], baseDir);
|
|
6241
|
+
references.push(...htmlRefs);
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
// Extract from script section (JS-like)
|
|
6245
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
|
|
6246
|
+
if (scriptMatch) {
|
|
6247
|
+
const jsRefs = this.extractJsReferences(scriptMatch[1], baseDir);
|
|
6248
|
+
references.push(...jsRefs);
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
// Extract from style section (CSS-like)
|
|
6252
|
+
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
6253
|
+
if (styleMatch) {
|
|
6254
|
+
const cssRefs = this.extractCssReferences(styleMatch[1], baseDir);
|
|
6255
|
+
references.push(...cssRefs);
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
return references;
|
|
6259
|
+
}
|
|
6260
|
+
|
|
6261
|
+
extractJsonReferences(content, baseDir) {
|
|
6262
|
+
const references = [];
|
|
6263
|
+
|
|
6264
|
+
try {
|
|
6265
|
+
// Parse JSON and look for string values that might be file paths
|
|
6266
|
+
const data = JSON.parse(content);
|
|
6267
|
+
|
|
6268
|
+
const findPaths = (obj) => {
|
|
6269
|
+
if (typeof obj === 'string') {
|
|
6270
|
+
// Check if string looks like a file path
|
|
6271
|
+
if (/\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico)$/i.test(obj)) {
|
|
6272
|
+
if (this.isLocalFile(obj)) {
|
|
6273
|
+
this.addNormalizedUrl(references, obj);
|
|
6274
|
+
}
|
|
6014
6275
|
}
|
|
6015
|
-
|
|
6016
|
-
|
|
6276
|
+
} else if (Array.isArray(obj)) {
|
|
6277
|
+
obj.forEach(item => findPaths(item));
|
|
6278
|
+
} else if (obj && typeof obj === 'object') {
|
|
6279
|
+
Object.values(obj).forEach(value => findPaths(value));
|
|
6280
|
+
}
|
|
6281
|
+
};
|
|
6282
|
+
|
|
6283
|
+
findPaths(data);
|
|
6284
|
+
} catch (error) {
|
|
6285
|
+
// If JSON parsing fails, try regex patterns
|
|
6286
|
+
const patterns = [
|
|
6287
|
+
/["']([^"']*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico))["']/gi
|
|
6288
|
+
];
|
|
6289
|
+
|
|
6290
|
+
for (const pattern of patterns) {
|
|
6291
|
+
let match;
|
|
6292
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
6293
|
+
const url = match[1];
|
|
6294
|
+
if (this.isLocalFile(url)) {
|
|
6295
|
+
this.addNormalizedUrl(references, url);
|
|
6017
6296
|
}
|
|
6018
6297
|
}
|
|
6019
6298
|
}
|
|
@@ -6022,6 +6301,68 @@ class AccessibilityFixer {
|
|
|
6022
6301
|
return references;
|
|
6023
6302
|
}
|
|
6024
6303
|
|
|
6304
|
+
extractXmlReferences(content, baseDir) {
|
|
6305
|
+
const references = [];
|
|
6306
|
+
|
|
6307
|
+
// XML/SVG patterns for file references
|
|
6308
|
+
const patterns = [
|
|
6309
|
+
// href attributes
|
|
6310
|
+
/href\s*=\s*["']([^"']+)["']/gi,
|
|
6311
|
+
// src attributes
|
|
6312
|
+
/src\s*=\s*["']([^"']+)["']/gi,
|
|
6313
|
+
// xlink:href (SVG)
|
|
6314
|
+
/xlink:href\s*=\s*["']([^"']+)["']/gi,
|
|
6315
|
+
// file attributes
|
|
6316
|
+
/file\s*=\s*["']([^"']+)["']/gi,
|
|
6317
|
+
// path attributes with file extensions
|
|
6318
|
+
/path\s*=\s*["']([^"']*\.(html|css|js|jpg|jpeg|png|gif|svg))["']/gi
|
|
6319
|
+
];
|
|
6320
|
+
|
|
6321
|
+
for (const pattern of patterns) {
|
|
6322
|
+
let match;
|
|
6323
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
6324
|
+
const url = match[1];
|
|
6325
|
+
if (this.isLocalFile(url)) {
|
|
6326
|
+
this.addNormalizedUrl(references, url);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
|
|
6331
|
+
return references;
|
|
6332
|
+
}
|
|
6333
|
+
|
|
6334
|
+
extractPhpReferences(content, baseDir) {
|
|
6335
|
+
const references = [];
|
|
6336
|
+
|
|
6337
|
+
// PHP can contain HTML, so extract HTML references first
|
|
6338
|
+
const htmlRefs = this.extractHtmlReferences(content, baseDir);
|
|
6339
|
+
references.push(...htmlRefs);
|
|
6340
|
+
|
|
6341
|
+
// PHP-specific patterns
|
|
6342
|
+
const patterns = [
|
|
6343
|
+
// include/require statements
|
|
6344
|
+
/(?:include|require|include_once|require_once)\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6345
|
+
// file_get_contents
|
|
6346
|
+
/file_get_contents\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6347
|
+
// readfile
|
|
6348
|
+
/readfile\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
6349
|
+
// fopen
|
|
6350
|
+
/fopen\s*\(\s*["']([^"']+)["']\s*,/gi
|
|
6351
|
+
];
|
|
6352
|
+
|
|
6353
|
+
for (const pattern of patterns) {
|
|
6354
|
+
let match;
|
|
6355
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
6356
|
+
const url = match[1];
|
|
6357
|
+
if (this.isLocalFile(url)) {
|
|
6358
|
+
this.addNormalizedUrl(references, url);
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
|
|
6363
|
+
return references;
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6025
6366
|
async findCssFiles(directory) {
|
|
6026
6367
|
const files = [];
|
|
6027
6368
|
|