gbu-accessibility-package 1.5.0 โ 3.0.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/README.md +97 -51
- package/cli.js +196 -101
- package/demo/advanced-test.html +44 -0
- package/demo/advanced-test.html.backup +44 -0
- package/demo/aria-label-test.html +32 -0
- package/demo/backup-test.html +18 -0
- package/demo/backup-test2.html +13 -0
- package/demo/backup-test3.html +12 -0
- package/demo/comprehensive-test.html +21 -0
- package/demo/comprehensive-test.html.backup +21 -0
- package/lib/fixer.js +725 -11
- package/package.json +8 -1
- package/demo/duplicate-roles.html.backup +0 -45
- package/demo/picture-test.html +0 -37
- package/demo/picture-test.html.backup +0 -37
- package/demo/sample.html.backup +0 -47
package/lib/fixer.js
CHANGED
|
@@ -563,7 +563,7 @@ class AccessibilityFixer {
|
|
|
563
563
|
}
|
|
564
564
|
];
|
|
565
565
|
|
|
566
|
-
// Check for images that need role="img"
|
|
566
|
+
// Check for images that need role="img" and aria-label
|
|
567
567
|
const images = content.match(/<img[^>]*>/gi) || [];
|
|
568
568
|
images.forEach((img, index) => {
|
|
569
569
|
if (!img.includes('role=')) {
|
|
@@ -573,6 +573,18 @@ class AccessibilityFixer {
|
|
|
573
573
|
element: img.substring(0, 100) + '...'
|
|
574
574
|
});
|
|
575
575
|
}
|
|
576
|
+
|
|
577
|
+
// Check for missing aria-label when alt exists
|
|
578
|
+
const hasAriaLabel = /aria-label\s*=/i.test(img);
|
|
579
|
+
const altMatch = img.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
580
|
+
|
|
581
|
+
if (!hasAriaLabel && altMatch && altMatch[1].trim()) {
|
|
582
|
+
issues.push({
|
|
583
|
+
type: '๐ท๏ธ Missing aria-label',
|
|
584
|
+
description: `Image ${index + 1} should have aria-label matching alt text`,
|
|
585
|
+
element: img.substring(0, 100) + '...'
|
|
586
|
+
});
|
|
587
|
+
}
|
|
576
588
|
});
|
|
577
589
|
|
|
578
590
|
// Check for button elements with onclick that need role
|
|
@@ -676,7 +688,17 @@ class AccessibilityFixer {
|
|
|
676
688
|
const pictureWithoutRole = pictureBlock.replace(/\s*role\s*=\s*["']img["']/i, '');
|
|
677
689
|
|
|
678
690
|
// Add role="img" to img element
|
|
679
|
-
|
|
691
|
+
let imgWithRole = imgTag.replace(/(<img[^>]*?)(\s*>)/i, '$1 role="img"$2');
|
|
692
|
+
|
|
693
|
+
// Also add aria-label if img has alt but no aria-label
|
|
694
|
+
const imgHasAriaLabel = /aria-label\s*=/i.test(imgWithRole);
|
|
695
|
+
const altMatch = imgWithRole.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
696
|
+
|
|
697
|
+
if (!imgHasAriaLabel && altMatch && altMatch[1].trim()) {
|
|
698
|
+
const altText = altMatch[1].trim();
|
|
699
|
+
imgWithRole = imgWithRole.replace(/(<img[^>]*?)(\s*>)/i, `$1 aria-label="${altText}"$2`);
|
|
700
|
+
console.log(chalk.yellow(` ๐ท๏ธ Added aria-label="${altText}" to moved img element`));
|
|
701
|
+
}
|
|
680
702
|
|
|
681
703
|
// Replace the img in the modified picture block
|
|
682
704
|
const updatedPictureBlock = pictureWithoutRole.replace(imgTag, imgWithRole);
|
|
@@ -698,16 +720,33 @@ class AccessibilityFixer {
|
|
|
698
720
|
// Fix picture elements with img children - move role from picture to img
|
|
699
721
|
fixed = this.fixPictureImgRoles(fixed);
|
|
700
722
|
|
|
701
|
-
// Fix all images - add role="img"
|
|
723
|
+
// Fix all images - add role="img" and aria-label
|
|
702
724
|
fixed = fixed.replace(
|
|
703
725
|
/<img([^>]*>)/gi,
|
|
704
|
-
(match
|
|
726
|
+
(match) => {
|
|
727
|
+
let updatedImg = match;
|
|
728
|
+
let hasChanges = false;
|
|
729
|
+
|
|
705
730
|
// Check if role attribute already exists
|
|
706
|
-
if (
|
|
707
|
-
|
|
731
|
+
if (!/role\s*=/i.test(match)) {
|
|
732
|
+
updatedImg = updatedImg.replace(/(<img[^>]*?)(\s*>)/i, '$1 role="img"$2');
|
|
733
|
+
console.log(chalk.yellow(` ๐ผ๏ธ Added role="img" to image element`));
|
|
734
|
+
hasChanges = true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Check if aria-label already exists
|
|
738
|
+
if (!/aria-label\s*=/i.test(match)) {
|
|
739
|
+
// Extract alt text to use for aria-label
|
|
740
|
+
const altMatch = match.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
741
|
+
if (altMatch && altMatch[1].trim()) {
|
|
742
|
+
const altText = altMatch[1].trim();
|
|
743
|
+
updatedImg = updatedImg.replace(/(<img[^>]*?)(\s*>)/i, `$1 aria-label="${altText}"$2`);
|
|
744
|
+
console.log(chalk.yellow(` ๐ท๏ธ Added aria-label="${altText}" to image element`));
|
|
745
|
+
hasChanges = true;
|
|
746
|
+
}
|
|
708
747
|
}
|
|
709
|
-
|
|
710
|
-
return
|
|
748
|
+
|
|
749
|
+
return updatedImg;
|
|
711
750
|
}
|
|
712
751
|
);
|
|
713
752
|
|
|
@@ -899,7 +938,12 @@ class AccessibilityFixer {
|
|
|
899
938
|
lang: [],
|
|
900
939
|
alt: [],
|
|
901
940
|
roles: [],
|
|
902
|
-
cleanup: []
|
|
941
|
+
cleanup: [],
|
|
942
|
+
forms: [],
|
|
943
|
+
buttons: [],
|
|
944
|
+
links: [],
|
|
945
|
+
landmarks: [],
|
|
946
|
+
headings: [] // Analysis only
|
|
903
947
|
};
|
|
904
948
|
|
|
905
949
|
try {
|
|
@@ -915,8 +959,28 @@ class AccessibilityFixer {
|
|
|
915
959
|
console.log(chalk.yellow('\n๐ญ Step 3: Role attributes...'));
|
|
916
960
|
results.roles = await this.fixRoleAttributes(directory);
|
|
917
961
|
|
|
918
|
-
// Step 4:
|
|
919
|
-
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...'));
|
|
920
984
|
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
921
985
|
|
|
922
986
|
// Summary
|
|
@@ -924,6 +988,10 @@ class AccessibilityFixer {
|
|
|
924
988
|
...results.lang.map(r => r.file),
|
|
925
989
|
...results.alt.map(r => r.file),
|
|
926
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),
|
|
927
995
|
...results.cleanup.map(r => r.file)
|
|
928
996
|
]).size;
|
|
929
997
|
|
|
@@ -931,6 +999,10 @@ class AccessibilityFixer {
|
|
|
931
999
|
...results.lang.filter(r => r.status === 'fixed').map(r => r.file),
|
|
932
1000
|
...results.alt.filter(r => r.status === 'fixed').map(r => r.file),
|
|
933
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),
|
|
934
1006
|
...results.cleanup.filter(r => r.status === 'fixed').map(r => r.file)
|
|
935
1007
|
]).size;
|
|
936
1008
|
|
|
@@ -938,6 +1010,10 @@ class AccessibilityFixer {
|
|
|
938
1010
|
results.lang.filter(r => r.status === 'fixed').length +
|
|
939
1011
|
results.alt.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
940
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) +
|
|
941
1017
|
results.cleanup.filter(r => r.status === 'fixed').length;
|
|
942
1018
|
|
|
943
1019
|
console.log(chalk.green('\n๐ All accessibility fixes completed!'));
|
|
@@ -958,6 +1034,644 @@ class AccessibilityFixer {
|
|
|
958
1034
|
}
|
|
959
1035
|
}
|
|
960
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
|
+
|
|
961
1675
|
async findHtmlFiles(directory) {
|
|
962
1676
|
const files = [];
|
|
963
1677
|
|