gbu-accessibility-package 1.6.0 โ 3.1.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 +124 -0
- package/README-vi.md +500 -0
- package/README.md +201 -115
- package/cli.js +174 -77
- package/demo/advanced-test.html +44 -0
- package/demo/advanced-test.html.backup +44 -0
- package/demo/comprehensive-test.html +21 -0
- package/demo/comprehensive-test.html.backup +21 -0
- package/lib/fixer.js +678 -3
- package/package.json +24 -5
package/lib/fixer.js
CHANGED
|
@@ -938,7 +938,12 @@ class AccessibilityFixer {
|
|
|
938
938
|
lang: [],
|
|
939
939
|
alt: [],
|
|
940
940
|
roles: [],
|
|
941
|
-
cleanup: []
|
|
941
|
+
cleanup: [],
|
|
942
|
+
forms: [],
|
|
943
|
+
buttons: [],
|
|
944
|
+
links: [],
|
|
945
|
+
landmarks: [],
|
|
946
|
+
headings: [] // Analysis only
|
|
942
947
|
};
|
|
943
948
|
|
|
944
949
|
try {
|
|
@@ -954,8 +959,28 @@ class AccessibilityFixer {
|
|
|
954
959
|
console.log(chalk.yellow('\n๐ญ Step 3: Role attributes...'));
|
|
955
960
|
results.roles = await this.fixRoleAttributes(directory);
|
|
956
961
|
|
|
957
|
-
// Step 4:
|
|
958
|
-
console.log(chalk.yellow('\n
|
|
962
|
+
// Step 4: Fix form labels
|
|
963
|
+
console.log(chalk.yellow('\n๐ Step 4: Form labels...'));
|
|
964
|
+
results.forms = await this.fixFormLabels(directory);
|
|
965
|
+
|
|
966
|
+
// Step 5: Fix button names
|
|
967
|
+
console.log(chalk.yellow('\n๐ Step 5: Button names...'));
|
|
968
|
+
results.buttons = await this.fixButtonNames(directory);
|
|
969
|
+
|
|
970
|
+
// Step 6: Fix link names
|
|
971
|
+
console.log(chalk.yellow('\n๐ Step 6: Link names...'));
|
|
972
|
+
results.links = await this.fixLinkNames(directory);
|
|
973
|
+
|
|
974
|
+
// Step 7: Fix landmarks
|
|
975
|
+
console.log(chalk.yellow('\n๐๏ธ Step 7: Landmarks...'));
|
|
976
|
+
results.landmarks = await this.fixLandmarks(directory);
|
|
977
|
+
|
|
978
|
+
// Step 8: Analyze headings (no auto-fix)
|
|
979
|
+
console.log(chalk.yellow('\n๐ Step 8: Heading analysis...'));
|
|
980
|
+
results.headings = await this.analyzeHeadings(directory);
|
|
981
|
+
|
|
982
|
+
// Step 9: Cleanup duplicate roles
|
|
983
|
+
console.log(chalk.yellow('\n๐งน Step 9: Cleanup duplicate roles...'));
|
|
959
984
|
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
960
985
|
|
|
961
986
|
// Summary
|
|
@@ -963,6 +988,10 @@ class AccessibilityFixer {
|
|
|
963
988
|
...results.lang.map(r => r.file),
|
|
964
989
|
...results.alt.map(r => r.file),
|
|
965
990
|
...results.roles.map(r => r.file),
|
|
991
|
+
...results.forms.map(r => r.file),
|
|
992
|
+
...results.buttons.map(r => r.file),
|
|
993
|
+
...results.links.map(r => r.file),
|
|
994
|
+
...results.landmarks.map(r => r.file),
|
|
966
995
|
...results.cleanup.map(r => r.file)
|
|
967
996
|
]).size;
|
|
968
997
|
|
|
@@ -970,6 +999,10 @@ class AccessibilityFixer {
|
|
|
970
999
|
...results.lang.filter(r => r.status === 'fixed').map(r => r.file),
|
|
971
1000
|
...results.alt.filter(r => r.status === 'fixed').map(r => r.file),
|
|
972
1001
|
...results.roles.filter(r => r.status === 'fixed').map(r => r.file),
|
|
1002
|
+
...results.forms.filter(r => r.status === 'fixed').map(r => r.file),
|
|
1003
|
+
...results.buttons.filter(r => r.status === 'fixed').map(r => r.file),
|
|
1004
|
+
...results.links.filter(r => r.status === 'fixed').map(r => r.file),
|
|
1005
|
+
...results.landmarks.filter(r => r.status === 'fixed').map(r => r.file),
|
|
973
1006
|
...results.cleanup.filter(r => r.status === 'fixed').map(r => r.file)
|
|
974
1007
|
]).size;
|
|
975
1008
|
|
|
@@ -977,6 +1010,10 @@ class AccessibilityFixer {
|
|
|
977
1010
|
results.lang.filter(r => r.status === 'fixed').length +
|
|
978
1011
|
results.alt.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
979
1012
|
results.roles.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
1013
|
+
results.forms.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
1014
|
+
results.buttons.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
1015
|
+
results.links.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
1016
|
+
results.landmarks.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
980
1017
|
results.cleanup.filter(r => r.status === 'fixed').length;
|
|
981
1018
|
|
|
982
1019
|
console.log(chalk.green('\n๐ All accessibility fixes completed!'));
|
|
@@ -997,6 +1034,644 @@ class AccessibilityFixer {
|
|
|
997
1034
|
}
|
|
998
1035
|
}
|
|
999
1036
|
|
|
1037
|
+
// Fix form labels
|
|
1038
|
+
async fixFormLabels(directory = '.') {
|
|
1039
|
+
console.log(chalk.blue('๐ Fixing form labels...'));
|
|
1040
|
+
|
|
1041
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1042
|
+
const results = [];
|
|
1043
|
+
let totalIssuesFound = 0;
|
|
1044
|
+
|
|
1045
|
+
for (const file of htmlFiles) {
|
|
1046
|
+
try {
|
|
1047
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1048
|
+
const issues = this.analyzeFormLabels(content);
|
|
1049
|
+
|
|
1050
|
+
if (issues.length > 0) {
|
|
1051
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
1052
|
+
issues.forEach(issue => {
|
|
1053
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
1054
|
+
totalIssuesFound++;
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const fixed = this.fixFormLabelsInContent(content);
|
|
1059
|
+
|
|
1060
|
+
if (fixed !== content) {
|
|
1061
|
+
if (this.config.backupFiles) {
|
|
1062
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (!this.config.dryRun) {
|
|
1066
|
+
await fs.writeFile(file, fixed);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
console.log(chalk.green(`โ
Fixed form labels in: ${file}`));
|
|
1070
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
1071
|
+
} else {
|
|
1072
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
1073
|
+
}
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
1076
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} form label issues across ${results.length} files`));
|
|
1081
|
+
return results;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
analyzeFormLabels(content) {
|
|
1085
|
+
const issues = [];
|
|
1086
|
+
|
|
1087
|
+
// Find input elements without labels
|
|
1088
|
+
const inputPattern = /<input[^>]*>/gi;
|
|
1089
|
+
const inputs = content.match(inputPattern) || [];
|
|
1090
|
+
|
|
1091
|
+
inputs.forEach((input, index) => {
|
|
1092
|
+
const hasId = /id\s*=\s*["']([^"']+)["']/i.test(input);
|
|
1093
|
+
const hasAriaLabel = /aria-label\s*=/i.test(input);
|
|
1094
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(input);
|
|
1095
|
+
const inputType = input.match(/type\s*=\s*["']([^"']+)["']/i);
|
|
1096
|
+
const type = inputType ? inputType[1].toLowerCase() : 'text';
|
|
1097
|
+
|
|
1098
|
+
// Skip certain input types that don't need labels
|
|
1099
|
+
if (['hidden', 'submit', 'button', 'reset'].includes(type)) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (hasId) {
|
|
1104
|
+
const idMatch = input.match(/id\s*=\s*["']([^"']+)["']/i);
|
|
1105
|
+
const id = idMatch[1];
|
|
1106
|
+
const labelPattern = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>`, 'i');
|
|
1107
|
+
const hasLabel = labelPattern.test(content);
|
|
1108
|
+
|
|
1109
|
+
if (!hasLabel && !hasAriaLabel && !hasAriaLabelledby) {
|
|
1110
|
+
issues.push({
|
|
1111
|
+
type: '๐ Missing label',
|
|
1112
|
+
description: `Input ${index + 1} (type: ${type}) needs a label or aria-label`,
|
|
1113
|
+
element: input.substring(0, 100) + '...'
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
} else if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
1117
|
+
issues.push({
|
|
1118
|
+
type: '๐ Missing label/id',
|
|
1119
|
+
description: `Input ${index + 1} (type: ${type}) needs an id and label, or aria-label`,
|
|
1120
|
+
element: input.substring(0, 100) + '...'
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
return issues;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
fixFormLabelsInContent(content) {
|
|
1129
|
+
let fixed = content;
|
|
1130
|
+
|
|
1131
|
+
// Add aria-label to inputs without labels (basic fix)
|
|
1132
|
+
const inputPattern = /<input([^>]*type\s*=\s*["']([^"']+)["'][^>]*)>/gi;
|
|
1133
|
+
|
|
1134
|
+
fixed = fixed.replace(inputPattern, (match, attributes, type) => {
|
|
1135
|
+
const lowerType = type.toLowerCase();
|
|
1136
|
+
|
|
1137
|
+
// Skip certain input types
|
|
1138
|
+
if (['hidden', 'submit', 'button', 'reset'].includes(lowerType)) {
|
|
1139
|
+
return match;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const hasAriaLabel = /aria-label\s*=/i.test(match);
|
|
1143
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
|
|
1144
|
+
const hasId = /id\s*=\s*["']([^"']+)["']/i.test(match);
|
|
1145
|
+
|
|
1146
|
+
if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
1147
|
+
// Check if there's a corresponding label
|
|
1148
|
+
if (hasId) {
|
|
1149
|
+
const idMatch = match.match(/id\s*=\s*["']([^"']+)["']/i);
|
|
1150
|
+
const id = idMatch[1];
|
|
1151
|
+
const labelPattern = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>`, 'i');
|
|
1152
|
+
|
|
1153
|
+
if (!labelPattern.test(content)) {
|
|
1154
|
+
// Add basic aria-label
|
|
1155
|
+
const labelText = this.generateInputLabel(lowerType);
|
|
1156
|
+
console.log(chalk.yellow(` ๐ Added aria-label="${labelText}" to ${lowerType} input`));
|
|
1157
|
+
return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 aria-label="${labelText}"$2`);
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
// Add basic aria-label
|
|
1161
|
+
const labelText = this.generateInputLabel(lowerType);
|
|
1162
|
+
console.log(chalk.yellow(` ๐ Added aria-label="${labelText}" to ${lowerType} input`));
|
|
1163
|
+
return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 aria-label="${labelText}"$2`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return match;
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
return fixed;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
generateInputLabel(type) {
|
|
1174
|
+
const labels = {
|
|
1175
|
+
'text': 'ใใญในใๅ
ฅๅ',
|
|
1176
|
+
'email': 'ใกใผใซใขใใฌใน',
|
|
1177
|
+
'password': 'ใในใฏใผใ',
|
|
1178
|
+
'tel': '้ป่ฉฑ็ชๅท',
|
|
1179
|
+
'url': 'URL',
|
|
1180
|
+
'search': 'ๆค็ดข',
|
|
1181
|
+
'number': 'ๆฐๅค',
|
|
1182
|
+
'date': 'ๆฅไป',
|
|
1183
|
+
'time': 'ๆ้',
|
|
1184
|
+
'checkbox': 'ใใงใใฏใใใฏใน',
|
|
1185
|
+
'radio': 'ใฉใธใชใใฟใณ',
|
|
1186
|
+
'file': 'ใใกใคใซ้ธๆ'
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
return labels[type] || 'ใใฉใผใ ๅ
ฅๅ';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Fix button names
|
|
1193
|
+
async fixButtonNames(directory = '.') {
|
|
1194
|
+
console.log(chalk.blue('๐ Fixing button names...'));
|
|
1195
|
+
|
|
1196
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1197
|
+
const results = [];
|
|
1198
|
+
let totalIssuesFound = 0;
|
|
1199
|
+
|
|
1200
|
+
for (const file of htmlFiles) {
|
|
1201
|
+
try {
|
|
1202
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1203
|
+
const issues = this.analyzeButtonNames(content);
|
|
1204
|
+
|
|
1205
|
+
if (issues.length > 0) {
|
|
1206
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
1207
|
+
issues.forEach(issue => {
|
|
1208
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
1209
|
+
totalIssuesFound++;
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const fixed = this.fixButtonNamesInContent(content);
|
|
1214
|
+
|
|
1215
|
+
if (fixed !== content) {
|
|
1216
|
+
if (this.config.backupFiles) {
|
|
1217
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (!this.config.dryRun) {
|
|
1221
|
+
await fs.writeFile(file, fixed);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
console.log(chalk.green(`โ
Fixed button names in: ${file}`));
|
|
1225
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
1226
|
+
} else {
|
|
1227
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
1228
|
+
}
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
1231
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} button name issues across ${results.length} files`));
|
|
1236
|
+
return results;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
analyzeButtonNames(content) {
|
|
1240
|
+
const issues = [];
|
|
1241
|
+
|
|
1242
|
+
// Find buttons without discernible text
|
|
1243
|
+
const buttonPattern = /<button[^>]*>[\s\S]*?<\/button>/gi;
|
|
1244
|
+
const buttons = content.match(buttonPattern) || [];
|
|
1245
|
+
|
|
1246
|
+
buttons.forEach((button, index) => {
|
|
1247
|
+
const hasAriaLabel = /aria-label\s*=/i.test(button);
|
|
1248
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(button);
|
|
1249
|
+
const hasTitle = /title\s*=/i.test(button);
|
|
1250
|
+
|
|
1251
|
+
// Extract text content
|
|
1252
|
+
const textContent = button.replace(/<[^>]*>/g, '').trim();
|
|
1253
|
+
|
|
1254
|
+
if (!textContent && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
|
|
1255
|
+
issues.push({
|
|
1256
|
+
type: '๐ Empty button',
|
|
1257
|
+
description: `Button ${index + 1} has no discernible text`,
|
|
1258
|
+
element: button.substring(0, 100) + '...'
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// Find input buttons without names
|
|
1264
|
+
const inputButtonPattern = /<input[^>]*type\s*=\s*["'](button|submit|reset)["'][^>]*>/gi;
|
|
1265
|
+
const inputButtons = content.match(inputButtonPattern) || [];
|
|
1266
|
+
|
|
1267
|
+
inputButtons.forEach((input, index) => {
|
|
1268
|
+
const hasValue = /value\s*=/i.test(input);
|
|
1269
|
+
const hasAriaLabel = /aria-label\s*=/i.test(input);
|
|
1270
|
+
const hasTitle = /title\s*=/i.test(input);
|
|
1271
|
+
|
|
1272
|
+
if (!hasValue && !hasAriaLabel && !hasTitle) {
|
|
1273
|
+
issues.push({
|
|
1274
|
+
type: '๐ Input button without name',
|
|
1275
|
+
description: `Input button ${index + 1} needs value, aria-label, or title`,
|
|
1276
|
+
element: input.substring(0, 100) + '...'
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
return issues;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fixButtonNamesInContent(content) {
|
|
1285
|
+
let fixed = content;
|
|
1286
|
+
|
|
1287
|
+
// Fix empty buttons
|
|
1288
|
+
fixed = fixed.replace(/<button([^>]*)>\s*<\/button>/gi, (match, attributes) => {
|
|
1289
|
+
const hasAriaLabel = /aria-label\s*=/i.test(match);
|
|
1290
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
|
|
1291
|
+
const hasTitle = /title\s*=/i.test(match);
|
|
1292
|
+
|
|
1293
|
+
if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
|
|
1294
|
+
console.log(chalk.yellow(` ๐ Added aria-label to empty button`));
|
|
1295
|
+
return `<button${attributes} aria-label="ใใฟใณ">ใใฟใณ</button>`;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return match;
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Fix input buttons without value
|
|
1302
|
+
fixed = fixed.replace(/<input([^>]*type\s*=\s*["'](button|submit|reset)["'][^>]*)>/gi, (match, attributes, type) => {
|
|
1303
|
+
const hasValue = /value\s*=/i.test(match);
|
|
1304
|
+
const hasAriaLabel = /aria-label\s*=/i.test(match);
|
|
1305
|
+
const hasTitle = /title\s*=/i.test(match);
|
|
1306
|
+
|
|
1307
|
+
if (!hasValue && !hasAriaLabel && !hasTitle) {
|
|
1308
|
+
const buttonText = type === 'submit' ? '้ไฟก' : type === 'reset' ? 'ใชใปใใ' : 'ใใฟใณ';
|
|
1309
|
+
console.log(chalk.yellow(` ๐ Added value="${buttonText}" to input ${type} button`));
|
|
1310
|
+
return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 value="${buttonText}"$2`);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return match;
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
return fixed;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Fix link names
|
|
1320
|
+
async fixLinkNames(directory = '.') {
|
|
1321
|
+
console.log(chalk.blue('๐ Fixing link names...'));
|
|
1322
|
+
|
|
1323
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1324
|
+
const results = [];
|
|
1325
|
+
let totalIssuesFound = 0;
|
|
1326
|
+
|
|
1327
|
+
for (const file of htmlFiles) {
|
|
1328
|
+
try {
|
|
1329
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1330
|
+
const issues = this.analyzeLinkNames(content);
|
|
1331
|
+
|
|
1332
|
+
if (issues.length > 0) {
|
|
1333
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
1334
|
+
issues.forEach(issue => {
|
|
1335
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
1336
|
+
totalIssuesFound++;
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const fixed = this.fixLinkNamesInContent(content);
|
|
1341
|
+
|
|
1342
|
+
if (fixed !== content) {
|
|
1343
|
+
if (this.config.backupFiles) {
|
|
1344
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (!this.config.dryRun) {
|
|
1348
|
+
await fs.writeFile(file, fixed);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
console.log(chalk.green(`โ
Fixed link names in: ${file}`));
|
|
1352
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
1353
|
+
} else {
|
|
1354
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
1355
|
+
}
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
1358
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} link name issues across ${results.length} files`));
|
|
1363
|
+
return results;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
analyzeLinkNames(content) {
|
|
1367
|
+
const issues = [];
|
|
1368
|
+
|
|
1369
|
+
// Find links without discernible text
|
|
1370
|
+
const linkPattern = /<a[^>]*href[^>]*>[\s\S]*?<\/a>/gi;
|
|
1371
|
+
const links = content.match(linkPattern) || [];
|
|
1372
|
+
|
|
1373
|
+
links.forEach((link, index) => {
|
|
1374
|
+
const hasAriaLabel = /aria-label\s*=/i.test(link);
|
|
1375
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(link);
|
|
1376
|
+
const hasTitle = /title\s*=/i.test(link);
|
|
1377
|
+
|
|
1378
|
+
// Extract text content (excluding images)
|
|
1379
|
+
let textContent = link.replace(/<img[^>]*>/gi, '').replace(/<[^>]*>/g, '').trim();
|
|
1380
|
+
|
|
1381
|
+
// Check for image alt text if link contains only images
|
|
1382
|
+
if (!textContent) {
|
|
1383
|
+
const imgMatch = link.match(/<img[^>]*alt\s*=\s*["']([^"']+)["'][^>]*>/i);
|
|
1384
|
+
if (imgMatch) {
|
|
1385
|
+
textContent = imgMatch[1].trim();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (!textContent && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
|
|
1390
|
+
issues.push({
|
|
1391
|
+
type: '๐ Empty link',
|
|
1392
|
+
description: `Link ${index + 1} has no discernible text`,
|
|
1393
|
+
element: link.substring(0, 100) + '...'
|
|
1394
|
+
});
|
|
1395
|
+
} else if (textContent && (textContent.toLowerCase() === 'click here' || textContent.toLowerCase() === 'read more' || textContent.toLowerCase() === 'here')) {
|
|
1396
|
+
issues.push({
|
|
1397
|
+
type: '๐ Generic link text',
|
|
1398
|
+
description: `Link ${index + 1} has generic text: "${textContent}"`,
|
|
1399
|
+
element: link.substring(0, 100) + '...'
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
return issues;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
fixLinkNamesInContent(content) {
|
|
1408
|
+
let fixed = content;
|
|
1409
|
+
|
|
1410
|
+
// Fix empty links
|
|
1411
|
+
fixed = fixed.replace(/<a([^>]*href[^>]*)>\s*<\/a>/gi, (match, attributes) => {
|
|
1412
|
+
const hasAriaLabel = /aria-label\s*=/i.test(match);
|
|
1413
|
+
const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
|
|
1414
|
+
const hasTitle = /title\s*=/i.test(match);
|
|
1415
|
+
|
|
1416
|
+
if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
|
|
1417
|
+
console.log(chalk.yellow(` ๐ Added aria-label to empty link`));
|
|
1418
|
+
return `<a${attributes} aria-label="ใชใณใฏ">ใชใณใฏ</a>`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return match;
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
// Fix links with only images but no alt text
|
|
1425
|
+
fixed = fixed.replace(/<a([^>]*href[^>]*)>(\s*<img[^>]*>\s*)<\/a>/gi, (match, attributes, imgTag) => {
|
|
1426
|
+
const hasAriaLabel = /aria-label\s*=/i.test(match);
|
|
1427
|
+
const hasAlt = /alt\s*=/i.test(imgTag);
|
|
1428
|
+
|
|
1429
|
+
if (!hasAriaLabel && !hasAlt) {
|
|
1430
|
+
console.log(chalk.yellow(` ๐ Added aria-label to image link`));
|
|
1431
|
+
return `<a${attributes} aria-label="็ปๅใชใณใฏ">${imgTag}</a>`;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return match;
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
return fixed;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Fix landmarks
|
|
1441
|
+
async fixLandmarks(directory = '.') {
|
|
1442
|
+
console.log(chalk.blue('๐๏ธ Fixing landmarks...'));
|
|
1443
|
+
|
|
1444
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1445
|
+
const results = [];
|
|
1446
|
+
let totalIssuesFound = 0;
|
|
1447
|
+
|
|
1448
|
+
for (const file of htmlFiles) {
|
|
1449
|
+
try {
|
|
1450
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1451
|
+
const issues = this.analyzeLandmarks(content);
|
|
1452
|
+
|
|
1453
|
+
if (issues.length > 0) {
|
|
1454
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
1455
|
+
issues.forEach(issue => {
|
|
1456
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
1457
|
+
totalIssuesFound++;
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const fixed = this.fixLandmarksInContent(content);
|
|
1462
|
+
|
|
1463
|
+
if (fixed !== content) {
|
|
1464
|
+
if (this.config.backupFiles) {
|
|
1465
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (!this.config.dryRun) {
|
|
1469
|
+
await fs.writeFile(file, fixed);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
console.log(chalk.green(`โ
Fixed landmarks in: ${file}`));
|
|
1473
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
1474
|
+
} else {
|
|
1475
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
1476
|
+
}
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
1479
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} landmark issues across ${results.length} files`));
|
|
1484
|
+
return results;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
analyzeLandmarks(content) {
|
|
1488
|
+
const issues = [];
|
|
1489
|
+
|
|
1490
|
+
// Check for main landmark
|
|
1491
|
+
const hasMain = /<main[^>]*>/i.test(content) || /role\s*=\s*["']main["']/i.test(content);
|
|
1492
|
+
if (!hasMain) {
|
|
1493
|
+
issues.push({
|
|
1494
|
+
type: '๐๏ธ Missing main landmark',
|
|
1495
|
+
description: 'Page should have a main landmark',
|
|
1496
|
+
suggestion: 'Add <main> element or role="main"'
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Check for multiple main landmarks
|
|
1501
|
+
const mainCount = (content.match(/<main[^>]*>/gi) || []).length +
|
|
1502
|
+
(content.match(/role\s*=\s*["']main["']/gi) || []).length;
|
|
1503
|
+
if (mainCount > 1) {
|
|
1504
|
+
issues.push({
|
|
1505
|
+
type: '๐๏ธ Multiple main landmarks',
|
|
1506
|
+
description: `Found ${mainCount} main landmarks, should have only one`,
|
|
1507
|
+
suggestion: 'Keep only one main landmark per page'
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Check for navigation landmarks
|
|
1512
|
+
const hasNav = /<nav[^>]*>/i.test(content) || /role\s*=\s*["']navigation["']/i.test(content);
|
|
1513
|
+
if (!hasNav) {
|
|
1514
|
+
// Look for navigation-like elements
|
|
1515
|
+
const navLikeElements = content.match(/<(?:ul|div)[^>]*class\s*=\s*["'][^"']*(?:nav|menu|navigation)[^"']*["'][^>]*>/gi);
|
|
1516
|
+
if (navLikeElements && navLikeElements.length > 0) {
|
|
1517
|
+
issues.push({
|
|
1518
|
+
type: '๐๏ธ Missing navigation landmark',
|
|
1519
|
+
description: 'Navigation elements should have nav tag or role="navigation"',
|
|
1520
|
+
suggestion: 'Use <nav> element or add role="navigation"'
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return issues;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
fixLandmarksInContent(content) {
|
|
1529
|
+
let fixed = content;
|
|
1530
|
+
|
|
1531
|
+
// Add main landmark if missing (basic implementation)
|
|
1532
|
+
const hasMain = /<main[^>]*>/i.test(content) || /role\s*=\s*["']main["']/i.test(content);
|
|
1533
|
+
|
|
1534
|
+
if (!hasMain) {
|
|
1535
|
+
// Look for content containers that could be main
|
|
1536
|
+
const contentPatterns = [
|
|
1537
|
+
/<div[^>]*class\s*=\s*["'][^"']*(?:content|main|container)[^"']*["'][^>]*>/gi,
|
|
1538
|
+
/<section[^>]*class\s*=\s*["'][^"']*(?:content|main)[^"']*["'][^>]*>/gi
|
|
1539
|
+
];
|
|
1540
|
+
|
|
1541
|
+
for (const pattern of contentPatterns) {
|
|
1542
|
+
const matches = fixed.match(pattern);
|
|
1543
|
+
if (matches && matches.length === 1) {
|
|
1544
|
+
// Add role="main" to the first suitable container
|
|
1545
|
+
fixed = fixed.replace(pattern, (match) => {
|
|
1546
|
+
if (!/role\s*=/i.test(match)) {
|
|
1547
|
+
console.log(chalk.yellow(` ๐๏ธ Added role="main" to content container`));
|
|
1548
|
+
return match.replace(/(<(?:div|section)[^>]*?)(\s*>)/i, '$1 role="main"$2');
|
|
1549
|
+
}
|
|
1550
|
+
return match;
|
|
1551
|
+
});
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Add navigation role to nav-like elements
|
|
1558
|
+
const navPattern = /<(?:ul|div)([^>]*class\s*=\s*["'][^"']*(?:nav|menu|navigation)[^"']*["'][^>]*)>/gi;
|
|
1559
|
+
fixed = fixed.replace(navPattern, (match, attributes) => {
|
|
1560
|
+
if (!/role\s*=/i.test(match)) {
|
|
1561
|
+
console.log(chalk.yellow(` ๐๏ธ Added role="navigation" to navigation element`));
|
|
1562
|
+
return match.replace(/(<(?:ul|div)[^>]*?)(\s*>)/i, '$1 role="navigation"$2');
|
|
1563
|
+
}
|
|
1564
|
+
return match;
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
return fixed;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Analyze headings (no auto-fix, only suggestions)
|
|
1571
|
+
async analyzeHeadings(directory = '.') {
|
|
1572
|
+
console.log(chalk.blue('๐ Analyzing heading structure...'));
|
|
1573
|
+
|
|
1574
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1575
|
+
const results = [];
|
|
1576
|
+
|
|
1577
|
+
for (const file of htmlFiles) {
|
|
1578
|
+
try {
|
|
1579
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1580
|
+
const issues = this.analyzeHeadingStructure(content);
|
|
1581
|
+
|
|
1582
|
+
if (issues.length > 0) {
|
|
1583
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
1584
|
+
issues.forEach(issue => {
|
|
1585
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
1586
|
+
if (issue.suggestion) {
|
|
1587
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
results.push({ file, status: 'analyzed', issues: issues.length, suggestions: issues });
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
1595
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
console.log(chalk.blue(`\n๐ Summary: Analyzed heading structure in ${results.length} files`));
|
|
1600
|
+
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
1601
|
+
return results;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
analyzeHeadingStructure(content) {
|
|
1605
|
+
const issues = [];
|
|
1606
|
+
|
|
1607
|
+
// Extract all headings with their levels and text
|
|
1608
|
+
const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
1609
|
+
const headings = [];
|
|
1610
|
+
let match;
|
|
1611
|
+
|
|
1612
|
+
while ((match = headingPattern.exec(content)) !== null) {
|
|
1613
|
+
const level = parseInt(match[1]);
|
|
1614
|
+
const text = match[2].replace(/<[^>]*>/g, '').trim();
|
|
1615
|
+
headings.push({ level, text, position: match.index });
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if (headings.length === 0) {
|
|
1619
|
+
issues.push({
|
|
1620
|
+
type: '๐ No headings found',
|
|
1621
|
+
description: 'Page has no heading elements',
|
|
1622
|
+
suggestion: 'Add heading elements (h1-h6) to structure content'
|
|
1623
|
+
});
|
|
1624
|
+
return issues;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Check for h1
|
|
1628
|
+
const hasH1 = headings.some(h => h.level === 1);
|
|
1629
|
+
if (!hasH1) {
|
|
1630
|
+
issues.push({
|
|
1631
|
+
type: '๐ Missing h1',
|
|
1632
|
+
description: 'Page should have exactly one h1 element',
|
|
1633
|
+
suggestion: 'Add an h1 element as the main page heading'
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Check for multiple h1s
|
|
1638
|
+
const h1Count = headings.filter(h => h.level === 1).length;
|
|
1639
|
+
if (h1Count > 1) {
|
|
1640
|
+
issues.push({
|
|
1641
|
+
type: '๐ Multiple h1 elements',
|
|
1642
|
+
description: `Found ${h1Count} h1 elements, should have only one`,
|
|
1643
|
+
suggestion: 'Use only one h1 per page, use h2-h6 for subheadings'
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Check heading order
|
|
1648
|
+
for (let i = 1; i < headings.length; i++) {
|
|
1649
|
+
const current = headings[i];
|
|
1650
|
+
const previous = headings[i - 1];
|
|
1651
|
+
|
|
1652
|
+
if (current.level > previous.level + 1) {
|
|
1653
|
+
issues.push({
|
|
1654
|
+
type: '๐ Heading level skip',
|
|
1655
|
+
description: `Heading level jumps from h${previous.level} to h${current.level}`,
|
|
1656
|
+
suggestion: `Use h${previous.level + 1} instead of h${current.level}, or add intermediate headings`
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Check for empty headings
|
|
1662
|
+
headings.forEach((heading, index) => {
|
|
1663
|
+
if (!heading.text) {
|
|
1664
|
+
issues.push({
|
|
1665
|
+
type: '๐ Empty heading',
|
|
1666
|
+
description: `Heading ${index + 1} (h${heading.level}) is empty`,
|
|
1667
|
+
suggestion: 'Add descriptive text to the heading or remove it'
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
return issues;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1000
1675
|
async findHtmlFiles(directory) {
|
|
1001
1676
|
const files = [];
|
|
1002
1677
|
|