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/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
- const imgWithRole = imgTag.replace(/(<img[^>]*?)(\s*>)/i, '$1 role="img"$2');
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" (only if no role exists)
723
+ // Fix all images - add role="img" and aria-label
702
724
  fixed = fixed.replace(
703
725
  /<img([^>]*>)/gi,
704
- (match, fullTag) => {
726
+ (match) => {
727
+ let updatedImg = match;
728
+ let hasChanges = false;
729
+
705
730
  // Check if role attribute already exists
706
- if (/role\s*=/i.test(match)) {
707
- return match; // Return unchanged if role already exists
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
- console.log(chalk.yellow(` ๐Ÿ–ผ๏ธ Added role="img" to image element`));
710
- return match.replace(/(<img[^>]*?)(\s*>)/i, '$1 role="img"$2');
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: Cleanup duplicate roles
919
- console.log(chalk.yellow('\n๐Ÿงน Step 4: Cleanup duplicate roles...'));
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