gbu-accessibility-package 3.12.1 → 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/index.js CHANGED
@@ -1,3 +1,6 @@
1
- const AccessibilityFixer = require('./lib/fixer.js');
1
+ const AccessibilityChecker = require('./lib/checker.js');
2
2
 
3
- module.exports = { AccessibilityFixer };
3
+ module.exports = {
4
+ AccessibilityChecker,
5
+ AccessibilityFixer: AccessibilityChecker
6
+ };
package/lib/checker.js ADDED
@@ -0,0 +1,233 @@
1
+ const AccessibilityFixer = require('./fixer.js');
2
+
3
+ class AccessibilityChecker extends AccessibilityFixer {
4
+ constructor(config = {}) {
5
+ super({
6
+ ...config,
7
+ dryRun: true,
8
+ backupFiles: false,
9
+ autoFixHeadings: false
10
+ });
11
+ }
12
+
13
+ transformLogValue(value) {
14
+ if (typeof value !== 'string') {
15
+ return value;
16
+ }
17
+
18
+ return value
19
+ .replace(/Đang sửa/g, 'Đang kiểm tra')
20
+ .replace(/Fixing/g, 'Checking')
21
+ .replace(/✅ Fixed/g, '✅ Reported')
22
+ .replace(/Files fixed/g, 'Files with findings')
23
+ .replace(/files fixed/g, 'files with findings')
24
+ .replace(/Tự động sửa/g, 'Rà soát')
25
+ .replace(/Sử dụng --meta-fix để tự động sửa các lỗi này/g, 'Các lỗi meta cần được review và xử lý thủ công')
26
+ .replace(/Use without --dry-run to apply changes\./g, 'Source files are never modified.');
27
+ }
28
+
29
+ async withCheckOnlyLogging(task) {
30
+ const originalLog = console.log;
31
+ const originalError = console.error;
32
+
33
+ console.log = (...messages) => originalLog(...messages.map((message) => this.transformLogValue(message)));
34
+ console.error = (...messages) => originalError(...messages.map((message) => this.transformLogValue(message)));
35
+
36
+ try {
37
+ return await task();
38
+ } finally {
39
+ console.log = originalLog;
40
+ console.error = originalError;
41
+ }
42
+ }
43
+
44
+ normalizeSimulationResults(results = []) {
45
+ return results.map((result) => {
46
+ if (result.status === 'fixed') {
47
+ return {
48
+ ...result,
49
+ status: 'issue-found',
50
+ rawStatus: result.status
51
+ };
52
+ }
53
+
54
+ if (result.status === 'no-change') {
55
+ return {
56
+ ...result,
57
+ status: 'clean',
58
+ rawStatus: result.status
59
+ };
60
+ }
61
+
62
+ return result;
63
+ });
64
+ }
65
+
66
+ async checkLang(directory = '.') {
67
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixHtmlLang(directory)));
68
+ }
69
+
70
+ async checkAltText(directory = '.') {
71
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixEmptyAltAttributes(directory)));
72
+ }
73
+
74
+ async checkRoles(directory = '.') {
75
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixRoleAttributes(directory)));
76
+ }
77
+
78
+ async checkAriaLabels(directory = '.') {
79
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixAriaLabels(directory)));
80
+ }
81
+
82
+ async checkForms(directory = '.') {
83
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixFormLabels(directory)));
84
+ }
85
+
86
+ async checkNestedControls(directory = '.') {
87
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixNestedInteractiveControls(directory)));
88
+ }
89
+
90
+ async checkButtons(directory = '.') {
91
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixButtonNames(directory)));
92
+ }
93
+
94
+ async checkLinks(directory = '.') {
95
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixLinkNames(directory)));
96
+ }
97
+
98
+ async checkLandmarks(directory = '.') {
99
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixLandmarks(directory)));
100
+ }
101
+
102
+ async checkHeadings(directory = '.') {
103
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixHeadingStructure(directory)));
104
+ }
105
+
106
+ async checkDescriptionLists(directory = '.') {
107
+ return this.withCheckOnlyLogging(async () => this.normalizeSimulationResults(await super.fixDescriptionLists(directory)));
108
+ }
109
+
110
+ async checkLinksAndResources(directory = '.') {
111
+ return this.withCheckOnlyLogging(async () => {
112
+ const [brokenLinks, missingResources] = await Promise.all([
113
+ this.checkBrokenLinks(directory),
114
+ this.check404Resources(directory)
115
+ ]);
116
+
117
+ return {
118
+ brokenLinks,
119
+ missingResources
120
+ };
121
+ });
122
+ }
123
+
124
+ async previewUnusedFilesListRemoval(directory = '.', listFile = 'unused-files-list.txt') {
125
+ return this.withCheckOnlyLogging(async () => super.deleteUnusedFilesFromList(directory, listFile, { dryRun: true }));
126
+ }
127
+
128
+ async checkMetaTags(directory = '.') {
129
+ return this.withCheckOnlyLogging(async () => super.checkMetaTags(directory));
130
+ }
131
+
132
+ async checkUnusedFiles(directory = '.') {
133
+ return this.withCheckOnlyLogging(async () => super.checkUnusedFiles(directory));
134
+ }
135
+
136
+ async generateUnusedFilesList(directory = '.', listFile = 'unused-files-list.txt') {
137
+ return this.withCheckOnlyLogging(async () => super.generateUnusedFilesList(directory, listFile));
138
+ }
139
+
140
+ async checkDeadCode(directory = '.') {
141
+ return this.withCheckOnlyLogging(async () => super.checkDeadCode(directory));
142
+ }
143
+
144
+ async checkFileSizes(directory = '.') {
145
+ return this.withCheckOnlyLogging(async () => super.checkFileSizes(directory));
146
+ }
147
+
148
+ async checkGoogleTagManager(directory = '.') {
149
+ return this.withCheckOnlyLogging(async () => super.checkGoogleTagManager(directory));
150
+ }
151
+
152
+ async generateFullReport(directory = '.', outputPath = null) {
153
+ return this.withCheckOnlyLogging(async () => super.generateFullReport(directory, outputPath));
154
+ }
155
+
156
+ async runComprehensiveChecks(directory = '.') {
157
+ const results = {};
158
+
159
+ results.lang = await this.checkLang(directory);
160
+ results.alt = await this.checkAltText(directory);
161
+ results.roles = await this.checkRoles(directory);
162
+ results.ariaLabels = await this.checkAriaLabels(directory);
163
+ results.forms = await this.checkForms(directory);
164
+ results.nestedControls = await this.checkNestedControls(directory);
165
+ results.buttons = await this.checkButtons(directory);
166
+ results.links = await this.checkLinks(directory);
167
+ results.landmarks = await this.checkLandmarks(directory);
168
+ results.headings = await this.checkHeadings(directory);
169
+ results.descriptionLists = await this.checkDescriptionLists(directory);
170
+ results.externalLinks = await this.withCheckOnlyLogging(async () => this.checkBrokenLinks(directory));
171
+ results.missingResources = await this.withCheckOnlyLogging(async () => this.check404Resources(directory));
172
+
173
+ return results;
174
+ }
175
+
176
+ async fixHtmlLang(directory = '.') {
177
+ return this.checkLang(directory);
178
+ }
179
+
180
+ async fixEmptyAltAttributes(directory = '.') {
181
+ return this.checkAltText(directory);
182
+ }
183
+
184
+ async fixRoleAttributes(directory = '.') {
185
+ return this.checkRoles(directory);
186
+ }
187
+
188
+ async fixAriaLabels(directory = '.') {
189
+ return this.checkAriaLabels(directory);
190
+ }
191
+
192
+ async fixFormLabels(directory = '.') {
193
+ return this.checkForms(directory);
194
+ }
195
+
196
+ async fixNestedInteractiveControls(directory = '.') {
197
+ return this.checkNestedControls(directory);
198
+ }
199
+
200
+ async fixButtonNames(directory = '.') {
201
+ return this.checkButtons(directory);
202
+ }
203
+
204
+ async fixLinkNames(directory = '.') {
205
+ return this.checkLinks(directory);
206
+ }
207
+
208
+ async fixLandmarks(directory = '.') {
209
+ return this.checkLandmarks(directory);
210
+ }
211
+
212
+ async fixHeadingStructure(directory = '.') {
213
+ return this.checkHeadings(directory);
214
+ }
215
+
216
+ async fixDescriptionLists(directory = '.') {
217
+ return this.checkDescriptionLists(directory);
218
+ }
219
+
220
+ async fixMetaTags(directory = '.') {
221
+ return this.checkMetaTags(directory);
222
+ }
223
+
224
+ async fixAllAccessibilityIssues(directory = '.') {
225
+ return this.runComprehensiveChecks(directory);
226
+ }
227
+
228
+ async deleteUnusedFilesFromList(directory = '.', listFile = 'unused-files-list.txt') {
229
+ return this.previewUnusedFilesListRemoval(directory, listFile);
230
+ }
231
+ }
232
+
233
+ module.exports = AccessibilityChecker;
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
@@ -6106,7 +6010,7 @@ class AccessibilityFixer {
6106
6010
  async deleteUnusedFilesFromList(directory = '.', listFile = 'unused-files-list.txt', options = {}) {
6107
6011
  const scanDirectory = path.resolve(directory);
6108
6012
  const listPath = this.resolveUnusedFilesListPath(scanDirectory, listFile);
6109
- const dryRun = Boolean(options.dryRun);
6013
+ const dryRun = true;
6110
6014
  const deletedFiles = [];
6111
6015
  const missingFiles = [];
6112
6016
  const skippedEntries = [];
@@ -6151,10 +6055,6 @@ class AccessibilityFixer {
6151
6055
  continue;
6152
6056
  }
6153
6057
 
6154
- if (!dryRun) {
6155
- await fs.unlink(resolvedPath);
6156
- }
6157
-
6158
6058
  deletedFiles.push({
6159
6059
  path: resolvedPath,
6160
6060
  relativePath: relativeToScan.replace(/\\/g, '/')
@@ -7804,6 +7704,7 @@ class AccessibilityFixer {
7804
7704
  { header: 'Loại lỗi', key: 'type', width: 20 },
7805
7705
  { header: 'Mô tả', key: 'description', width: 60 },
7806
7706
  { header: 'Element', key: 'element', width: 40 },
7707
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7807
7708
  { header: 'Mức độ', key: 'severity', width: 15 }
7808
7709
  ];
7809
7710
 
@@ -7819,6 +7720,7 @@ class AccessibilityFixer {
7819
7720
  type: issue.type,
7820
7721
  description: issue.description,
7821
7722
  element: issue.element || '',
7723
+ suggestion: this.getAccessibilitySuggestion(issue),
7822
7724
  severity: '❌ Lỗi'
7823
7725
  });
7824
7726
  row.getCell('severity').style = errorStyle;
@@ -7826,7 +7728,7 @@ class AccessibilityFixer {
7826
7728
  }
7827
7729
  }
7828
7730
 
7829
- accessSheet.autoFilter = 'A1:E1';
7731
+ accessSheet.autoFilter = 'A1:F1';
7830
7732
  console.log(chalk.green(` ✅ Accessibility: ${accessResults.length} file có vấn đề`));
7831
7733
  } catch (error) {
7832
7734
  console.log(chalk.yellow(` ⚠️ Accessibility: ${error.message}`));
@@ -7844,6 +7746,7 @@ class AccessibilityFixer {
7844
7746
  { header: 'Loại lỗi', key: 'type', width: 25 },
7845
7747
  { header: 'Mô tả', key: 'description', width: 60 },
7846
7748
  { header: 'Element', key: 'element', width: 40 },
7749
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7847
7750
  { header: 'Mức độ', key: 'severity', width: 15 }
7848
7751
  ];
7849
7752
 
@@ -7859,6 +7762,7 @@ class AccessibilityFixer {
7859
7762
  type: issue.type,
7860
7763
  description: issue.description,
7861
7764
  element: issue.element || '',
7765
+ suggestion: this.getFormSuggestion(issue),
7862
7766
  severity: '❌ Lỗi'
7863
7767
  });
7864
7768
  row.getCell('severity').style = errorStyle;
@@ -7866,7 +7770,7 @@ class AccessibilityFixer {
7866
7770
  }
7867
7771
  }
7868
7772
 
7869
- formsSheet.autoFilter = 'A1:E1';
7773
+ formsSheet.autoFilter = 'A1:F1';
7870
7774
  console.log(chalk.green(` ✅ Forms: ${formsResults.length} file có vấn đề`));
7871
7775
  } catch (error) {
7872
7776
  console.log(chalk.yellow(` ⚠️ Forms: ${error.message}`));
@@ -7884,6 +7788,7 @@ class AccessibilityFixer {
7884
7788
  { header: 'Loại lỗi', key: 'type', width: 25 },
7885
7789
  { header: 'Mô tả', key: 'description', width: 60 },
7886
7790
  { header: 'Element', key: 'element', width: 40 },
7791
+ { header: 'Gợi ý xử lý', key: 'suggestion', width: 50 },
7887
7792
  { header: 'Mức độ', key: 'severity', width: 15 }
7888
7793
  ];
7889
7794
 
@@ -7899,6 +7804,7 @@ class AccessibilityFixer {
7899
7804
  type: issue.type,
7900
7805
  description: issue.description,
7901
7806
  element: issue.element || '',
7807
+ suggestion: this.getButtonSuggestion(issue),
7902
7808
  severity: '❌ Lỗi'
7903
7809
  });
7904
7810
  row.getCell('severity').style = errorStyle;
@@ -7906,7 +7812,7 @@ class AccessibilityFixer {
7906
7812
  }
7907
7813
  }
7908
7814
 
7909
- buttonsSheet.autoFilter = 'A1:E1';
7815
+ buttonsSheet.autoFilter = 'A1:F1';
7910
7816
  console.log(chalk.green(` ✅ Buttons: ${buttonsResults.length} file có vấn đề`));
7911
7817
  } catch (error) {
7912
7818
  console.log(chalk.yellow(` ⚠️ Buttons: ${error.message}`));
@@ -7970,7 +7876,8 @@ class AccessibilityFixer {
7970
7876
  { header: 'URL/Đường dẫn', key: 'url', width: 60 },
7971
7877
  { header: 'Loại', key: 'type', width: 20 },
7972
7878
  { header: 'Trạng thái', key: 'status', width: 15 },
7973
- { 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 }
7974
7881
  ];
7975
7882
 
7976
7883
  linksSheet.getRow(1).eachCell(cell => {
@@ -7983,7 +7890,8 @@ class AccessibilityFixer {
7983
7890
  url: link.url,
7984
7891
  type: 'External Link',
7985
7892
  status: link.status || 'Broken',
7986
- 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')
7987
7895
  });
7988
7896
  row.getCell('status').style = errorStyle;
7989
7897
  allResults.summary.totalIssues++;
@@ -7995,13 +7903,14 @@ class AccessibilityFixer {
7995
7903
  url: resource.path,
7996
7904
  type: 'Local Resource',
7997
7905
  status: '404',
7998
- 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')
7999
7908
  });
8000
7909
  row.getCell('status').style = errorStyle;
8001
7910
  allResults.summary.totalIssues++;
8002
7911
  }
8003
7912
 
8004
- linksSheet.autoFilter = 'A1:E1';
7913
+ linksSheet.autoFilter = 'A1:F1';
8005
7914
  console.log(chalk.green(` ✅ Broken Links: ${allResults.brokenLinks.length} link, ${allResults.missingResources.length} resource`));
8006
7915
  } catch (error) {
8007
7916
  console.log(chalk.yellow(` ⚠️ Broken Links: ${error.message}`));
@@ -8018,7 +7927,8 @@ class AccessibilityFixer {
8018
7927
  { header: 'File', key: 'file', width: 60 },
8019
7928
  { header: 'Loại', key: 'type', width: 20 },
8020
7929
  { header: 'Kích thước', key: 'size', width: 15 },
8021
- { 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 }
8022
7932
  ];
8023
7933
 
8024
7934
  unusedSheet.getRow(1).eachCell(cell => {
@@ -8034,13 +7944,14 @@ class AccessibilityFixer {
8034
7944
  file: filePath,
8035
7945
  type: fileType,
8036
7946
  size: fileSize,
8037
- 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.'
8038
7949
  });
8039
7950
  row.getCell('note').style = warningStyle;
8040
7951
  allResults.summary.totalWarnings++;
8041
7952
  }
8042
7953
 
8043
- unusedSheet.autoFilter = 'A1:D1';
7954
+ unusedSheet.autoFilter = 'A1:E1';
8044
7955
  console.log(chalk.green(` ✅ Unused Files: ${unusedResults.length} file`));
8045
7956
  } catch (error) {
8046
7957
  console.log(chalk.yellow(` ⚠️ Unused Files: ${error.message}`));
@@ -8096,7 +8007,8 @@ class AccessibilityFixer {
8096
8007
  { header: 'Head Script', key: 'head', width: 15 },
8097
8008
  { header: 'Body Noscript', key: 'body', width: 15 },
8098
8009
  { header: 'Container ID', key: 'containerId', width: 30 },
8099
- { 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 }
8100
8012
  ];
8101
8013
 
8102
8014
  gtmSheet.getRow(1).eachCell(cell => {
@@ -8110,7 +8022,8 @@ class AccessibilityFixer {
8110
8022
  head: item.hasHeadScript ? '✅' : '❌',
8111
8023
  body: item.hasBodyNoscript ? '✅' : '❌',
8112
8024
  containerId: item.containerId || '-',
8113
- issues: (item.issues || []).join('; ') || 'Không có vấn đề'
8025
+ issues: (item.issues || []).join('; ') || 'Không có vấn đề',
8026
+ suggestion: this.getGTMReportSuggestion(item)
8114
8027
  });
8115
8028
  if (!item.hasGTM || item.issues?.length > 0) {
8116
8029
  row.getCell('status').style = errorStyle;
@@ -8120,7 +8033,7 @@ class AccessibilityFixer {
8120
8033
  }
8121
8034
  }
8122
8035
 
8123
- gtmSheet.autoFilter = 'A1:F1';
8036
+ gtmSheet.autoFilter = 'A1:G1';
8124
8037
  console.log(chalk.green(` ✅ GTM: ${gtmResults.length} file`));
8125
8038
  } catch (error) {
8126
8039
  console.log(chalk.yellow(` ⚠️ GTM: ${error.message}`));
@@ -8190,6 +8103,77 @@ class AccessibilityFixer {
8190
8103
  }
8191
8104
 
8192
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
+
8193
8177
  async checkMetaTagsForReport(directory) {
8194
8178
  const htmlFiles = await this.findHtmlFiles(directory);
8195
8179
  const results = [];