gbu-accessibility-package 3.12.0 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fixer.js CHANGED
@@ -1229,18 +1229,22 @@ class EnhancedAltChecker {
1229
1229
  class AccessibilityFixer {
1230
1230
  constructor(config = {}) {
1231
1231
  this.config = {
1232
- backupFiles: config.backupFiles === true,
1232
+ backupFiles: false,
1233
1233
  language: config.language || 'ja',
1234
- dryRun: config.dryRun || false,
1234
+ dryRun: true,
1235
1235
  enhancedAltMode: config.enhancedAltMode || false,
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
+ autoFixHeadings: false,
1240
+ fixDescriptionLists: config.fixDescriptionLists !== false,
1241
+ checkOnlyMode: true,
1242
1242
  ...config
1243
1243
  };
1244
+ this.config.backupFiles = false;
1245
+ this.config.dryRun = true;
1246
+ this.config.autoFixHeadings = false;
1247
+ this.config.checkOnlyMode = true;
1244
1248
 
1245
1249
  // Initialize enhanced alt tools
1246
1250
  this.enhancedAltChecker = new EnhancedAltChecker({
@@ -5886,108 +5890,8 @@ class AccessibilityFixer {
5886
5890
 
5887
5891
  // Fix meta tags in HTML files
5888
5892
  async fixMetaTags(directory = '.', options = {}) {
5889
- const { dryRun = false, backup = false } = options;
5890
- console.log(chalk.blue('🔧 Fixing meta tag typos and syntax errors...'));
5891
-
5892
- const htmlFiles = await this.findHtmlFiles(directory);
5893
- let totalFiles = 0;
5894
- let fixedFiles = 0;
5895
- let skippedFiles = 0;
5896
- let totalFixes = 0;
5897
-
5898
- for (const file of htmlFiles) {
5899
- try {
5900
- const content = await fs.readFile(file, 'utf8');
5901
- const analysis = this.analyzeMetaTags(content, file);
5902
- const relativePath = path.relative(directory, file);
5903
-
5904
- if (analysis.isIncludeFile) {
5905
- skippedFiles++;
5906
- console.log(chalk.gray(`⏭️ Skipped: ${relativePath} (include file)`));
5907
- continue;
5908
- }
5909
-
5910
- totalFiles++;
5911
-
5912
- if (analysis.fixable.length === 0) {
5913
- console.log(chalk.green(`✅ ${relativePath} - No errors to fix`));
5914
- continue;
5915
- }
5916
-
5917
- console.log(chalk.yellow(`\n🔧 Fixing: ${relativePath}`));
5918
-
5919
- let newContent = content;
5920
- let fixCount = 0;
5921
-
5922
- // Group fixes by tag to handle multiple fixes on same tag
5923
- const tagFixes = new Map();
5924
- for (const fix of analysis.fixable) {
5925
- if (!tagFixes.has(fix.fullTag)) {
5926
- tagFixes.set(fix.fullTag, []);
5927
- }
5928
- tagFixes.get(fix.fullTag).push(fix);
5929
- }
5930
-
5931
- // Apply all fixes for each tag
5932
- for (const [originalTag, fixes] of tagFixes) {
5933
- let newTag = originalTag;
5934
-
5935
- for (const fix of fixes) {
5936
- if (fix.type === 'property') {
5937
- // Fix property name typo
5938
- newTag = newTag.replace(
5939
- new RegExp(`((?:name|property)\\s*=\\s*["'])${fix.wrong.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(["'])`, 'i'),
5940
- `$1${fix.correct}$2`
5941
- );
5942
- console.log(chalk.green(` ✓ Fixed property: ${fix.wrong} → ${fix.correct}`));
5943
- fixCount++;
5944
- } else if (fix.type === 'content') {
5945
- // Fix content value typo
5946
- newTag = newTag.replace(
5947
- new RegExp(`(content\\s*=\\s*["'])${fix.wrong.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(["'])`, 'i'),
5948
- `$1${fix.correct}$2`
5949
- );
5950
- console.log(chalk.green(` ✓ Fixed ${fix.property} value: ${fix.wrong} → ${fix.correct}`));
5951
- fixCount++;
5952
- }
5953
- }
5954
-
5955
- // Replace the original tag with the fixed tag
5956
- newContent = newContent.replace(originalTag, newTag);
5957
- }
5958
-
5959
- // Save file if modified
5960
- if (fixCount > 0) {
5961
- if (!dryRun) {
5962
- // Create backup if enabled
5963
- if (backup) {
5964
- const backupPath = `${file}.bak`;
5965
- await fs.writeFile(backupPath, content, 'utf8');
5966
- }
5967
-
5968
- await fs.writeFile(file, newContent, 'utf8');
5969
- console.log(chalk.green(` 💾 Saved ${fixCount} fix(es) to ${relativePath}`));
5970
- fixedFiles++;
5971
- totalFixes += fixCount;
5972
- } else {
5973
- console.log(chalk.blue(` 🔍 Dry run - would fix ${fixCount} error(s) in ${relativePath}`));
5974
- totalFixes += fixCount;
5975
- }
5976
- }
5977
- } catch (error) {
5978
- console.log(chalk.red(`❌ Error processing ${file}: ${error.message}`));
5979
- }
5980
- }
5981
-
5982
- console.log(chalk.blue('\n📊 Summary:'));
5983
- console.log(` Total files checked: ${totalFiles}`);
5984
- console.log(` Files skipped (includes): ${skippedFiles}`);
5985
- console.log(chalk.green(` Files fixed: ${fixedFiles}`));
5986
- console.log(chalk.green(` Total fixes applied: ${totalFixes}`));
5987
-
5988
- if (dryRun) {
5989
- console.log(chalk.blue('\n💡 This was a dry run. Use without --dry-run to apply changes.'));
5990
- }
5893
+ console.log(chalk.yellow('⚠️ Check-only mode: auto-fixing meta tags has been removed. Running analysis only.'));
5894
+ return this.checkMetaTags(directory, options);
5991
5895
  }
5992
5896
 
5993
5897
  // Check for unused files in the project - enhanced for comprehensive project-wide scanning
@@ -6073,6 +5977,130 @@ class AccessibilityFixer {
6073
5977
  };
6074
5978
  }
6075
5979
 
5980
+ resolveUnusedFilesListPath(directory = '.', listFile = 'unused-files-list.txt') {
5981
+ const scanDirectory = path.resolve(directory);
5982
+ const listFileName = listFile || 'unused-files-list.txt';
5983
+
5984
+ if (path.isAbsolute(listFileName)) {
5985
+ return listFileName;
5986
+ }
5987
+
5988
+ return path.join(scanDirectory, listFileName);
5989
+ }
5990
+
5991
+ async generateUnusedFilesList(directory = '.', listFile = 'unused-files-list.txt') {
5992
+ const scanDirectory = path.resolve(directory);
5993
+ const outputPath = this.resolveUnusedFilesListPath(scanDirectory, listFile);
5994
+ const unusedResults = await this.checkUnusedFiles(scanDirectory);
5995
+ const fileContent = unusedResults.unusedFiles
5996
+ .map(file => file.relativePath.replace(/\\/g, '/'))
5997
+ .join('\n');
5998
+
5999
+ await fs.writeFile(outputPath, fileContent ? `${fileContent}\n` : '', 'utf8');
6000
+
6001
+ console.log(chalk.green(`📝 Đã tạo danh sách file dư thừa tại: ${outputPath}`));
6002
+ console.log(chalk.gray(`📊 Ghi ${unusedResults.unusedCount} path vào file list`));
6003
+
6004
+ return {
6005
+ ...unusedResults,
6006
+ outputPath
6007
+ };
6008
+ }
6009
+
6010
+ async deleteUnusedFilesFromList(directory = '.', listFile = 'unused-files-list.txt', options = {}) {
6011
+ const scanDirectory = path.resolve(directory);
6012
+ const listPath = this.resolveUnusedFilesListPath(scanDirectory, listFile);
6013
+ const dryRun = true;
6014
+ const deletedFiles = [];
6015
+ const missingFiles = [];
6016
+ const skippedEntries = [];
6017
+
6018
+ const content = await fs.readFile(listPath, 'utf8');
6019
+ const entries = [...new Set(
6020
+ content
6021
+ .split(/\r?\n/)
6022
+ .map(line => line.trim())
6023
+ .filter(line => line && !line.startsWith('#'))
6024
+ )];
6025
+
6026
+ for (const entry of entries) {
6027
+ const normalizedEntry = entry.replace(/\\/g, '/');
6028
+ const resolvedPath = path.resolve(scanDirectory, normalizedEntry);
6029
+ const relativeToScan = path.relative(scanDirectory, resolvedPath);
6030
+
6031
+ if (relativeToScan.startsWith('..') || path.isAbsolute(relativeToScan)) {
6032
+ skippedEntries.push({
6033
+ path: entry,
6034
+ reason: 'Path nằm ngoài thư mục target'
6035
+ });
6036
+ continue;
6037
+ }
6038
+
6039
+ if (resolvedPath === path.resolve(listPath)) {
6040
+ skippedEntries.push({
6041
+ path: entry,
6042
+ reason: 'Bỏ qua chính file list'
6043
+ });
6044
+ continue;
6045
+ }
6046
+
6047
+ try {
6048
+ const stats = await fs.lstat(resolvedPath);
6049
+
6050
+ if (!stats.isFile() && !stats.isSymbolicLink()) {
6051
+ skippedEntries.push({
6052
+ path: entry,
6053
+ reason: 'Không phải file'
6054
+ });
6055
+ continue;
6056
+ }
6057
+
6058
+ deletedFiles.push({
6059
+ path: resolvedPath,
6060
+ relativePath: relativeToScan.replace(/\\/g, '/')
6061
+ });
6062
+ } catch (error) {
6063
+ if (error.code === 'ENOENT') {
6064
+ missingFiles.push({
6065
+ path: entry,
6066
+ reason: 'File không tồn tại'
6067
+ });
6068
+ continue;
6069
+ }
6070
+
6071
+ skippedEntries.push({
6072
+ path: entry,
6073
+ reason: error.message
6074
+ });
6075
+ }
6076
+ }
6077
+
6078
+ console.log(chalk.green(
6079
+ dryRun
6080
+ ? `🧪 Dry run: ${deletedFiles.length} file trong list sẽ được xóa`
6081
+ : `🗑️ Đã xóa ${deletedFiles.length} file từ list`
6082
+ ));
6083
+
6084
+ if (missingFiles.length > 0) {
6085
+ console.log(chalk.yellow(`⚠️ ${missingFiles.length} path không còn tồn tại`));
6086
+ }
6087
+
6088
+ if (skippedEntries.length > 0) {
6089
+ console.log(chalk.yellow(`⚠️ ${skippedEntries.length} path bị bỏ qua để đảm bảo an toàn hoặc không hợp lệ`));
6090
+ }
6091
+
6092
+ return {
6093
+ listPath,
6094
+ deletedFiles,
6095
+ deletedCount: deletedFiles.length,
6096
+ missingFiles,
6097
+ missingCount: missingFiles.length,
6098
+ skippedEntries,
6099
+ skippedCount: skippedEntries.length,
6100
+ dryRun
6101
+ };
6102
+ }
6103
+
6076
6104
  // Enhanced file reference checking - now works with relative paths
6077
6105
  isFileReferenced(filePath, relativePath, referencedFiles, projectRoot) {
6078
6106
  // Check various possible reference formats
@@ -6310,12 +6338,17 @@ class AccessibilityFixer {
6310
6338
  const srcUrl = parts[0];
6311
6339
 
6312
6340
  if (srcUrl) {
6341
+ const normalizedSrcUrl = this.normalizeReferenceUrl(srcUrl);
6342
+ if (!normalizedSrcUrl) {
6343
+ return;
6344
+ }
6345
+
6313
6346
  // Check if it's a local file
6314
- if (this.isLocalFile(srcUrl)) {
6315
- this.addNormalizedUrl(references, srcUrl);
6316
- } else if (this.isAbsoluteUrlToLocalFile(srcUrl)) {
6347
+ if (this.isLocalFile(normalizedSrcUrl)) {
6348
+ this.addNormalizedUrl(references, normalizedSrcUrl);
6349
+ } else if (this.isAbsoluteUrlToLocalFile(normalizedSrcUrl)) {
6317
6350
  // Extract local path from absolute URL (e.g., https://example.com/assets/img/file.png -> /assets/img/file.png)
6318
- const localPath = this.extractLocalPathFromAbsoluteUrl(srcUrl);
6351
+ const localPath = this.extractLocalPathFromAbsoluteUrl(normalizedSrcUrl);
6319
6352
  if (localPath) {
6320
6353
  this.addNormalizedUrl(references, localPath);
6321
6354
  }
@@ -6323,12 +6356,17 @@ class AccessibilityFixer {
6323
6356
  }
6324
6357
  });
6325
6358
  } else {
6359
+ const normalizedUrl = this.normalizeReferenceUrl(url);
6360
+ if (!normalizedUrl) {
6361
+ continue;
6362
+ }
6363
+
6326
6364
  // Regular src/href/content attributes
6327
- if (this.isLocalFile(url)) {
6328
- this.addNormalizedUrl(references, url);
6329
- } else if (this.isAbsoluteUrlToLocalFile(url)) {
6365
+ if (this.isLocalFile(normalizedUrl)) {
6366
+ this.addNormalizedUrl(references, normalizedUrl);
6367
+ } else if (this.isAbsoluteUrlToLocalFile(normalizedUrl)) {
6330
6368
  // Extract local path from absolute URL
6331
- const localPath = this.extractLocalPathFromAbsoluteUrl(url);
6369
+ const localPath = this.extractLocalPathFromAbsoluteUrl(normalizedUrl);
6332
6370
  if (localPath) {
6333
6371
  this.addNormalizedUrl(references, localPath);
6334
6372
  }
@@ -6342,43 +6380,75 @@ class AccessibilityFixer {
6342
6380
 
6343
6381
  // Check if URL is an absolute URL that points to a local file (same domain or project assets)
6344
6382
  isAbsoluteUrlToLocalFile(url) {
6345
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
6383
+ const normalizedUrl = this.normalizeReferenceUrl(url);
6384
+ if (!normalizedUrl) {
6385
+ return false;
6386
+ }
6387
+
6388
+ if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
6346
6389
  return false;
6347
6390
  }
6348
6391
 
6349
6392
  // Check if URL contains common asset paths (customize based on your project structure)
6350
6393
  // This helps identify URLs like: https://www.example.com/assets/img/file.png
6351
- return url.includes('/assets/') ||
6352
- url.includes('/static/') ||
6353
- url.includes('/img/') ||
6354
- url.includes('/images/') ||
6355
- url.includes('/css/') ||
6356
- url.includes('/js/') ||
6357
- url.includes('/media/') ||
6358
- url.includes('/fonts/') ||
6359
- url.match(/\.(jpg|jpeg|png|gif|svg|webp|css|js|ico|pdf|mp4|webm)$/i);
6394
+ return normalizedUrl.includes('/assets/') ||
6395
+ normalizedUrl.includes('/static/') ||
6396
+ normalizedUrl.includes('/img/') ||
6397
+ normalizedUrl.includes('/images/') ||
6398
+ normalizedUrl.includes('/css/') ||
6399
+ normalizedUrl.includes('/js/') ||
6400
+ normalizedUrl.includes('/media/') ||
6401
+ normalizedUrl.includes('/fonts/') ||
6402
+ normalizedUrl.match(/\.(jpg|jpeg|png|gif|svg|webp|css|js|ico|pdf|mp4|webm)$/i);
6360
6403
  }
6361
6404
 
6362
6405
  // Extract local path from absolute URL
6363
6406
  // e.g., "https://www.example.com/assets/img/file.png" -> "/assets/img/file.png"
6364
6407
  extractLocalPathFromAbsoluteUrl(url) {
6365
6408
  try {
6366
- const urlObj = new URL(url);
6409
+ const normalizedUrl = this.normalizeReferenceUrl(url);
6410
+ if (!normalizedUrl) {
6411
+ return null;
6412
+ }
6413
+
6414
+ const urlObj = new URL(normalizedUrl);
6367
6415
  return urlObj.pathname; // Returns "/assets/img/file.png"
6368
6416
  } catch (error) {
6369
6417
  return null;
6370
6418
  }
6371
6419
  }
6372
6420
 
6421
+ normalizeReferenceUrl(url) {
6422
+ if (typeof url !== 'string') {
6423
+ return null;
6424
+ }
6425
+
6426
+ const trimmedUrl = url.trim();
6427
+ if (!trimmedUrl) {
6428
+ return null;
6429
+ }
6430
+
6431
+ const withoutHash = trimmedUrl.split('#')[0];
6432
+ const withoutQuery = withoutHash.split('?')[0];
6433
+ const normalizedUrl = withoutQuery.trim().replace(/\\/g, '/');
6434
+
6435
+ return normalizedUrl || null;
6436
+ }
6437
+
6373
6438
  // Helper method to add normalized URL variations
6374
6439
  addNormalizedUrl(references, url) {
6440
+ const normalizedUrl = this.normalizeReferenceUrl(url);
6441
+ if (!normalizedUrl) {
6442
+ return;
6443
+ }
6444
+
6375
6445
  // Store original URL and normalized versions for matching
6376
- references.push(url);
6377
- if (url.startsWith('/')) {
6378
- references.push(url.substring(1)); // Remove leading slash
6446
+ references.push(normalizedUrl);
6447
+ if (normalizedUrl.startsWith('/')) {
6448
+ references.push(normalizedUrl.substring(1)); // Remove leading slash
6379
6449
  }
6380
- if (!url.startsWith('./') && !url.startsWith('/')) {
6381
- references.push('./' + url); // Add leading ./
6450
+ if (!normalizedUrl.startsWith('./') && !normalizedUrl.startsWith('/')) {
6451
+ references.push('./' + normalizedUrl); // Add leading ./
6382
6452
  }
6383
6453
  }
6384
6454
 
@@ -6398,7 +6468,7 @@ class AccessibilityFixer {
6398
6468
  for (const pattern of patterns) {
6399
6469
  let match;
6400
6470
  while ((match = pattern.exec(content)) !== null) {
6401
- const url = match[1];
6471
+ const url = this.normalizeReferenceUrl(match[1]);
6402
6472
  if (this.isLocalFile(url)) {
6403
6473
  this.addNormalizedUrl(references, url);
6404
6474
  }
@@ -6424,15 +6494,15 @@ class AccessibilityFixer {
6424
6494
  // XMLHttpRequest
6425
6495
  /\.open\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']+)["']/gi,
6426
6496
  // String literals that look like paths
6427
- /["']([^"']*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico))["']/gi,
6497
+ /["']([^"']*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico)(?:[?#][^"']*)?)["']/gi,
6428
6498
  // Template literals with paths
6429
- /`([^`]*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico))`/gi
6499
+ /`([^`]*\.(html|css|js|json|xml|jpg|jpeg|png|gif|svg|webp|ico)(?:[?#][^`]*)?)`/gi
6430
6500
  ];
6431
6501
 
6432
6502
  for (const pattern of patterns) {
6433
6503
  let match;
6434
6504
  while ((match = pattern.exec(content)) !== null) {
6435
- const url = match[1];
6505
+ const url = this.normalizeReferenceUrl(match[1]);
6436
6506
  if (this.isLocalFile(url)) {
6437
6507
  this.addNormalizedUrl(references, url);
6438
6508
  }
@@ -6532,7 +6602,7 @@ class AccessibilityFixer {
6532
6602
  for (const pattern of patterns) {
6533
6603
  let match;
6534
6604
  while ((match = pattern.exec(content)) !== null) {
6535
- const url = match[1];
6605
+ const url = this.normalizeReferenceUrl(match[1]);
6536
6606
  if (this.isLocalFile(url)) {
6537
6607
  this.addNormalizedUrl(references, url);
6538
6608
  }
@@ -6564,7 +6634,7 @@ class AccessibilityFixer {
6564
6634
  for (const pattern of patterns) {
6565
6635
  let match;
6566
6636
  while ((match = pattern.exec(content)) !== null) {
6567
- const url = match[1];
6637
+ const url = this.normalizeReferenceUrl(match[1]);
6568
6638
  if (this.isLocalFile(url)) {
6569
6639
  this.addNormalizedUrl(references, url);
6570
6640
  }
@@ -6698,13 +6768,18 @@ class AccessibilityFixer {
6698
6768
  }
6699
6769
 
6700
6770
  isLocalFile(url) {
6701
- return !url.startsWith('http://') &&
6702
- !url.startsWith('https://') &&
6703
- !url.startsWith('//') &&
6704
- !url.startsWith('data:') &&
6705
- !url.startsWith('mailto:') &&
6706
- !url.startsWith('tel:') &&
6707
- !url.startsWith('#');
6771
+ const normalizedUrl = this.normalizeReferenceUrl(url);
6772
+ if (!normalizedUrl) {
6773
+ return false;
6774
+ }
6775
+
6776
+ return !normalizedUrl.startsWith('http://') &&
6777
+ !normalizedUrl.startsWith('https://') &&
6778
+ !normalizedUrl.startsWith('//') &&
6779
+ !normalizedUrl.startsWith('data:') &&
6780
+ !normalizedUrl.startsWith('mailto:') &&
6781
+ !normalizedUrl.startsWith('tel:') &&
6782
+ !normalizedUrl.startsWith('#');
6708
6783
  }
6709
6784
 
6710
6785
  resolveFilePath(url, baseDir) {
@@ -7378,9 +7453,14 @@ class AccessibilityFixer {
7378
7453
  // Extract file references from JSON values
7379
7454
  const extractFromObject = (obj) => {
7380
7455
  if (typeof obj === 'string') {
7456
+ const normalizedValue = this.normalizeReferenceUrl(obj);
7457
+ if (!normalizedValue) {
7458
+ return;
7459
+ }
7460
+
7381
7461
  // Check if it looks like a file path
7382
- if (obj.includes('.') && (obj.includes('/') || obj.includes('\\'))) {
7383
- const resolved = this.resolveFilePath(obj, baseDir);
7462
+ if (/\.(html?|css|js|json|xml|jpe?g|png|gif|svg|webp|ico)$/i.test(normalizedValue)) {
7463
+ const resolved = this.resolveFilePath(normalizedValue, baseDir);
7384
7464
  if (resolved) {
7385
7465
  references.push(resolved);
7386
7466
  }
@@ -7419,7 +7499,7 @@ class AccessibilityFixer {
7419
7499
  patterns.forEach(pattern => {
7420
7500
  let match;
7421
7501
  while ((match = pattern.exec(content)) !== null) {
7422
- const filePath = match[1];
7502
+ const filePath = this.normalizeReferenceUrl(match[1]);
7423
7503
 
7424
7504
  if (this.isLocalFile(filePath)) {
7425
7505
  const resolved = this.resolveFilePath(filePath, baseDir);
@@ -7624,6 +7704,7 @@ class AccessibilityFixer {
7624
7704
  { header: 'Loại lỗi', key: 'type', width: 20 },
7625
7705
  { header: 'Mô tả', key: 'description', width: 60 },
7626
7706
  { header: 'Element', key: 'element', width: 40 },
7707
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7627
7708
  { header: 'Mức độ', key: 'severity', width: 15 }
7628
7709
  ];
7629
7710
 
@@ -7639,6 +7720,7 @@ class AccessibilityFixer {
7639
7720
  type: issue.type,
7640
7721
  description: issue.description,
7641
7722
  element: issue.element || '',
7723
+ suggestion: this.getAccessibilitySuggestion(issue),
7642
7724
  severity: '❌ Lỗi'
7643
7725
  });
7644
7726
  row.getCell('severity').style = errorStyle;
@@ -7646,7 +7728,7 @@ class AccessibilityFixer {
7646
7728
  }
7647
7729
  }
7648
7730
 
7649
- accessSheet.autoFilter = 'A1:E1';
7731
+ accessSheet.autoFilter = 'A1:F1';
7650
7732
  console.log(chalk.green(` ✅ Accessibility: ${accessResults.length} file có vấn đề`));
7651
7733
  } catch (error) {
7652
7734
  console.log(chalk.yellow(` ⚠️ Accessibility: ${error.message}`));
@@ -7664,6 +7746,7 @@ class AccessibilityFixer {
7664
7746
  { header: 'Loại lỗi', key: 'type', width: 25 },
7665
7747
  { header: 'Mô tả', key: 'description', width: 60 },
7666
7748
  { header: 'Element', key: 'element', width: 40 },
7749
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7667
7750
  { header: 'Mức độ', key: 'severity', width: 15 }
7668
7751
  ];
7669
7752
 
@@ -7679,6 +7762,7 @@ class AccessibilityFixer {
7679
7762
  type: issue.type,
7680
7763
  description: issue.description,
7681
7764
  element: issue.element || '',
7765
+ suggestion: this.getFormSuggestion(issue),
7682
7766
  severity: '❌ Lỗi'
7683
7767
  });
7684
7768
  row.getCell('severity').style = errorStyle;
@@ -7686,7 +7770,7 @@ class AccessibilityFixer {
7686
7770
  }
7687
7771
  }
7688
7772
 
7689
- formsSheet.autoFilter = 'A1:E1';
7773
+ formsSheet.autoFilter = 'A1:F1';
7690
7774
  console.log(chalk.green(` ✅ Forms: ${formsResults.length} file có vấn đề`));
7691
7775
  } catch (error) {
7692
7776
  console.log(chalk.yellow(` ⚠️ Forms: ${error.message}`));
@@ -7704,6 +7788,7 @@ class AccessibilityFixer {
7704
7788
  { header: 'Loại lỗi', key: 'type', width: 25 },
7705
7789
  { header: 'Mô tả', key: 'description', width: 60 },
7706
7790
  { header: 'Element', key: 'element', width: 40 },
7791
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7707
7792
  { header: 'Mức độ', key: 'severity', width: 15 }
7708
7793
  ];
7709
7794
 
@@ -7719,6 +7804,7 @@ class AccessibilityFixer {
7719
7804
  type: issue.type,
7720
7805
  description: issue.description,
7721
7806
  element: issue.element || '',
7807
+ suggestion: this.getButtonSuggestion(issue),
7722
7808
  severity: '❌ Lỗi'
7723
7809
  });
7724
7810
  row.getCell('severity').style = errorStyle;
@@ -7726,7 +7812,7 @@ class AccessibilityFixer {
7726
7812
  }
7727
7813
  }
7728
7814
 
7729
- buttonsSheet.autoFilter = 'A1:E1';
7815
+ buttonsSheet.autoFilter = 'A1:F1';
7730
7816
  console.log(chalk.green(` ✅ Buttons: ${buttonsResults.length} file có vấn đề`));
7731
7817
  } catch (error) {
7732
7818
  console.log(chalk.yellow(` ⚠️ Buttons: ${error.message}`));
@@ -7790,7 +7876,8 @@ class AccessibilityFixer {
7790
7876
  { header: 'URL/Đường dẫn', key: 'url', width: 60 },
7791
7877
  { header: 'Loại', key: 'type', width: 20 },
7792
7878
  { header: 'Trạng thái', key: 'status', width: 15 },
7793
- { header: 'Mô tả', key: 'description', width: 40 }
7879
+ { header: 'Mô tả', key: 'description', width: 40 },
7880
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 }
7794
7881
  ];
7795
7882
 
7796
7883
  linksSheet.getRow(1).eachCell(cell => {
@@ -7803,7 +7890,8 @@ class AccessibilityFixer {
7803
7890
  url: link.url,
7804
7891
  type: 'External Link',
7805
7892
  status: link.status || 'Broken',
7806
- description: link.error || 'Link không thể truy cập'
7893
+ description: link.error || 'Link không thể truy cập',
7894
+ suggestion: link.suggestion || this.getLinkReportSuggestion(link, 'external')
7807
7895
  });
7808
7896
  row.getCell('status').style = errorStyle;
7809
7897
  allResults.summary.totalIssues++;
@@ -7815,13 +7903,14 @@ class AccessibilityFixer {
7815
7903
  url: resource.path,
7816
7904
  type: 'Local Resource',
7817
7905
  status: '404',
7818
- description: 'File không tồn tại'
7906
+ description: 'File không tồn tại',
7907
+ suggestion: resource.suggestion || this.getLinkReportSuggestion(resource, 'local')
7819
7908
  });
7820
7909
  row.getCell('status').style = errorStyle;
7821
7910
  allResults.summary.totalIssues++;
7822
7911
  }
7823
7912
 
7824
- linksSheet.autoFilter = 'A1:E1';
7913
+ linksSheet.autoFilter = 'A1:F1';
7825
7914
  console.log(chalk.green(` ✅ Broken Links: ${allResults.brokenLinks.length} link, ${allResults.missingResources.length} resource`));
7826
7915
  } catch (error) {
7827
7916
  console.log(chalk.yellow(` ⚠️ Broken Links: ${error.message}`));
@@ -7838,7 +7927,8 @@ class AccessibilityFixer {
7838
7927
  { header: 'File', key: 'file', width: 60 },
7839
7928
  { header: 'Loại', key: 'type', width: 20 },
7840
7929
  { header: 'Kích thước', key: 'size', width: 15 },
7841
- { header: 'Ghi chú', key: 'note', width: 40 }
7930
+ { header: 'Ghi chú', key: 'note', width: 40 },
7931
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 }
7842
7932
  ];
7843
7933
 
7844
7934
  unusedSheet.getRow(1).eachCell(cell => {
@@ -7854,13 +7944,14 @@ class AccessibilityFixer {
7854
7944
  file: filePath,
7855
7945
  type: fileType,
7856
7946
  size: fileSize,
7857
- note: 'Không được tham chiếu trong project'
7947
+ note: 'Không được tham chiếu trong project',
7948
+ suggestion: typeof file === 'object' ? (file.suggestion || 'Xác minh file có đang được dùng động không; nếu không cần, xóa thủ công khỏi project.') : 'Xác minh file có đang được dùng động không; nếu không cần, xóa thủ công khỏi project.'
7858
7949
  });
7859
7950
  row.getCell('note').style = warningStyle;
7860
7951
  allResults.summary.totalWarnings++;
7861
7952
  }
7862
7953
 
7863
- unusedSheet.autoFilter = 'A1:D1';
7954
+ unusedSheet.autoFilter = 'A1:E1';
7864
7955
  console.log(chalk.green(` ✅ Unused Files: ${unusedResults.length} file`));
7865
7956
  } catch (error) {
7866
7957
  console.log(chalk.yellow(` ⚠️ Unused Files: ${error.message}`));
@@ -7916,7 +8007,8 @@ class AccessibilityFixer {
7916
8007
  { header: 'Head Script', key: 'head', width: 15 },
7917
8008
  { header: 'Body Noscript', key: 'body', width: 15 },
7918
8009
  { header: 'Container ID', key: 'containerId', width: 30 },
7919
- { header: 'Vấn đề', key: 'issues', width: 50 }
8010
+ { header: 'Vấn đề', key: 'issues', width: 50 },
8011
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 }
7920
8012
  ];
7921
8013
 
7922
8014
  gtmSheet.getRow(1).eachCell(cell => {
@@ -7930,7 +8022,8 @@ class AccessibilityFixer {
7930
8022
  head: item.hasHeadScript ? '✅' : '❌',
7931
8023
  body: item.hasBodyNoscript ? '✅' : '❌',
7932
8024
  containerId: item.containerId || '-',
7933
- issues: (item.issues || []).join('; ') || 'Không có vấn đề'
8025
+ issues: (item.issues || []).join('; ') || 'Không có vấn đề',
8026
+ suggestion: this.getGTMReportSuggestion(item)
7934
8027
  });
7935
8028
  if (!item.hasGTM || item.issues?.length > 0) {
7936
8029
  row.getCell('status').style = errorStyle;
@@ -7940,7 +8033,7 @@ class AccessibilityFixer {
7940
8033
  }
7941
8034
  }
7942
8035
 
7943
- gtmSheet.autoFilter = 'A1:F1';
8036
+ gtmSheet.autoFilter = 'A1:G1';
7944
8037
  console.log(chalk.green(` ✅ GTM: ${gtmResults.length} file`));
7945
8038
  } catch (error) {
7946
8039
  console.log(chalk.yellow(` ⚠️ GTM: ${error.message}`));
@@ -8010,6 +8103,77 @@ class AccessibilityFixer {
8010
8103
  }
8011
8104
 
8012
8105
  // Helper methods for report generation
8106
+ getAccessibilitySuggestion(issue = {}) {
8107
+ const type = (issue.type || '').toLowerCase();
8108
+
8109
+ if (type.includes('missing alt')) {
8110
+ return 'Thêm thuộc tính alt mô tả đúng nội dung/chức năng của ảnh; nếu ảnh chỉ để trang trí, dùng alt="" và cân nhắc role="presentation".';
8111
+ }
8112
+
8113
+ if (type.includes('empty alt')) {
8114
+ return 'Điền alt text có ý nghĩa cho ảnh, hoặc nếu ảnh thực sự chỉ để trang trí thì giữ alt="" và đánh dấu rõ là decorative.';
8115
+ }
8116
+
8117
+ if (type.includes('empty aria-label')) {
8118
+ return 'Điền aria-label ngắn gọn nhưng đủ nghĩa, hoặc thay bằng text hiển thị/aria-labelledby nếu phần tử đã có nội dung liên kết rõ ràng.';
8119
+ }
8120
+
8121
+ return 'Xem lại accessible name và semantic của phần tử, rồi cập nhật text thay thế hoặc ARIA cho phù hợp với chức năng thực tế.';
8122
+ }
8123
+
8124
+ getFormSuggestion(issue = {}) {
8125
+ const type = (issue.type || '').toLowerCase();
8126
+
8127
+ if (type.includes('missing form label')) {
8128
+ return 'Thêm <label for="..."> liên kết với input, hoặc bổ sung aria-label/aria-labelledby nếu không thể dùng label hiển thị.';
8129
+ }
8130
+
8131
+ return 'Đảm bảo mỗi trường nhập liệu có accessible name rõ ràng và không phụ thuộc riêng vào placeholder.';
8132
+ }
8133
+
8134
+ getButtonSuggestion(issue = {}) {
8135
+ const type = (issue.type || '').toLowerCase();
8136
+
8137
+ if (type.includes('empty button')) {
8138
+ return 'Thêm text hiển thị cho button, hoặc bổ sung aria-label nếu button chỉ chứa icon nhưng vẫn cần tên truy cập được.';
8139
+ }
8140
+
8141
+ return 'Đảm bảo button có accessible name rõ ràng, ngắn gọn, và phản ánh đúng hành động của nó.';
8142
+ }
8143
+
8144
+ getLinkReportSuggestion(item = {}, mode = 'external') {
8145
+ if (item.suggestion) {
8146
+ return item.suggestion;
8147
+ }
8148
+
8149
+ if (mode === 'local') {
8150
+ return 'Kiểm tra lại path tham chiếu, tạo file còn thiếu, hoặc cập nhật/xóa reference không còn dùng.';
8151
+ }
8152
+
8153
+ return 'Kiểm tra lại URL đích, cập nhật link mới nếu trang đã đổi, hoặc gỡ liên kết nếu tài nguyên không còn tồn tại.';
8154
+ }
8155
+
8156
+ getGTMReportSuggestion(item = {}) {
8157
+ if (!item.hasGTM) {
8158
+ return 'Thêm đầy đủ GTM script trong <head> và GTM noscript ngay sau thẻ mở <body> nếu trang này cần được tracking.';
8159
+ }
8160
+
8161
+ const suggestions = [];
8162
+ const issues = item.issues || [];
8163
+
8164
+ for (const issue of issues) {
8165
+ if (issue.includes('Thiếu GTM script')) {
8166
+ suggestions.push('Bổ sung GTM script vào <head>, ưu tiên đặt ngay trước </head>.');
8167
+ } else if (issue.includes('Thiếu GTM noscript')) {
8168
+ suggestions.push('Bổ sung GTM noscript ngay sau thẻ mở <body>.');
8169
+ } else if (issue.includes('không khớp')) {
8170
+ suggestions.push('Đồng bộ số lượng script/noscript với các container ID GTM đang dùng trên trang.');
8171
+ }
8172
+ }
8173
+
8174
+ return suggestions.join(' ') || 'Xác minh lại cấu hình GTM giữa phần head, body noscript, và container ID.';
8175
+ }
8176
+
8013
8177
  async checkMetaTagsForReport(directory) {
8014
8178
  const htmlFiles = await this.findHtmlFiles(directory);
8015
8179
  const results = [];
@@ -8635,4 +8799,4 @@ class AccessibilityFixer {
8635
8799
  }
8636
8800
  }
8637
8801
 
8638
- module.exports = AccessibilityFixer;
8802
+ module.exports = AccessibilityFixer;