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/CHANGELOG.md +17 -1
- package/QUICK_START.md +14 -157
- package/README-vi.md +104 -672
- package/README.md +105 -652
- package/bin/fix.js +1 -138
- package/cli.js +297 -535
- package/index.js +5 -2
- package/lib/checker.js +233 -0
- package/lib/fixer.js +325 -161
- package/package.json +14 -13
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
|
|
@@ -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(
|
|
6315
|
-
this.addNormalizedUrl(references,
|
|
6316
|
-
} else if (this.isAbsoluteUrlToLocalFile(
|
|
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(
|
|
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(
|
|
6328
|
-
this.addNormalizedUrl(references,
|
|
6329
|
-
} else if (this.isAbsoluteUrlToLocalFile(
|
|
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(
|
|
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
|
-
|
|
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
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
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
|
|
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(
|
|
6377
|
-
if (
|
|
6378
|
-
references.push(
|
|
6446
|
+
references.push(normalizedUrl);
|
|
6447
|
+
if (normalizedUrl.startsWith('/')) {
|
|
6448
|
+
references.push(normalizedUrl.substring(1)); // Remove leading slash
|
|
6379
6449
|
}
|
|
6380
|
-
if (!
|
|
6381
|
-
references.push('./' +
|
|
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
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
!
|
|
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 (
|
|
7383
|
-
const resolved = this.resolveFilePath(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|