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/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: Cleanup duplicate roles
958
- 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...'));
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