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/CHANGELOG.md +9 -4
- package/QUICK_START.md +14 -157
- package/README-vi.md +104 -678
- package/README.md +105 -658
- package/bin/fix.js +1 -138
- package/cli.js +289 -578
- package/index.js +5 -2
- package/lib/checker.js +233 -0
- package/lib/fixer.js +109 -125
- package/package.json +13 -14
package/index.js
CHANGED
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:
|
|
1232
|
+
backupFiles: false,
|
|
1233
1233
|
language: config.language || 'ja',
|
|
1234
|
-
dryRun:
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
5890
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 = [];
|