gbu-accessibility-package 3.2.1 โ†’ 3.4.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
@@ -3106,6 +3106,371 @@ class AccessibilityFixer {
3106
3106
  return results;
3107
3107
  }
3108
3108
 
3109
+ async fixFormLabels(directory = '.') {
3110
+ console.log(chalk.blue('๐Ÿ“‹ Fixing form labels...'));
3111
+
3112
+ const htmlFiles = await this.findHtmlFiles(directory);
3113
+ const results = [];
3114
+ let totalIssuesFound = 0;
3115
+
3116
+ for (const file of htmlFiles) {
3117
+ try {
3118
+ const content = await fs.readFile(file, 'utf8');
3119
+ const issues = this.analyzeFormLabels(content);
3120
+
3121
+ if (issues.length > 0) {
3122
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3123
+ issues.forEach(issue => {
3124
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3125
+ totalIssuesFound++;
3126
+ });
3127
+ }
3128
+
3129
+ const fixed = this.fixFormLabelsInContent(content);
3130
+
3131
+ if (fixed !== content) {
3132
+ if (this.config.backupFiles) {
3133
+ await fs.writeFile(`${file}.backup`, content);
3134
+ }
3135
+
3136
+ if (!this.config.dryRun) {
3137
+ await fs.writeFile(file, fixed);
3138
+ }
3139
+
3140
+ console.log(chalk.green(`โœ… Fixed form labels in: ${file}`));
3141
+ results.push({ file, status: 'fixed', issues: issues.length });
3142
+ } else {
3143
+ results.push({ file, status: 'no-change', issues: issues.length });
3144
+ }
3145
+ } catch (error) {
3146
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
3147
+ results.push({ file, status: 'error', error: error.message });
3148
+ }
3149
+ }
3150
+
3151
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} form label issues across ${results.length} files`));
3152
+ return results;
3153
+ }
3154
+
3155
+ analyzeFormLabels(content) {
3156
+ const issues = [];
3157
+
3158
+ // Find all form input elements that need labels
3159
+ const inputElements = [
3160
+ 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
3161
+ 'input[type="tel"]', 'input[type="url"]', 'input[type="search"]',
3162
+ 'input[type="number"]', 'input[type="date"]', 'input[type="time"]',
3163
+ 'input[type="datetime-local"]', 'input[type="month"]', 'input[type="week"]',
3164
+ 'input[type="color"]', 'input[type="range"]', 'input[type="file"]',
3165
+ 'textarea', 'select'
3166
+ ];
3167
+
3168
+ // Convert to regex patterns
3169
+ const inputPatterns = [
3170
+ /<input[^>]*type\s*=\s*["'](?:text|email|password|tel|url|search|number|date|time|datetime-local|month|week|color|range|file)["'][^>]*>/gi,
3171
+ /<textarea[^>]*>/gi,
3172
+ /<select[^>]*>/gi
3173
+ ];
3174
+
3175
+ inputPatterns.forEach((pattern, patternIndex) => {
3176
+ const matches = content.match(pattern) || [];
3177
+
3178
+ matches.forEach((element, index) => {
3179
+ const elementType = patternIndex === 0 ? 'input' :
3180
+ patternIndex === 1 ? 'textarea' : 'select';
3181
+
3182
+ const issues_found = this.checkFormElementLabeling(element, content, elementType, index + 1);
3183
+ issues.push(...issues_found);
3184
+ });
3185
+ });
3186
+
3187
+ return issues;
3188
+ }
3189
+
3190
+ checkFormElementLabeling(element, content, elementType, index) {
3191
+ const issues = [];
3192
+
3193
+ // Extract element attributes
3194
+ const id = this.extractAttributeValue(element, 'id');
3195
+ const name = this.extractAttributeValue(element, 'name');
3196
+ const ariaLabel = this.extractAttributeValue(element, 'aria-label');
3197
+ const ariaLabelledby = this.extractAttributeValue(element, 'aria-labelledby');
3198
+ const title = this.extractAttributeValue(element, 'title');
3199
+ const placeholder = this.extractAttributeValue(element, 'placeholder');
3200
+
3201
+ let hasValidLabel = false;
3202
+ let labelMethods = [];
3203
+
3204
+ // Check for explicit label (label[for="id"])
3205
+ if (id) {
3206
+ const explicitLabelRegex = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>([^<]+)</label>`, 'i');
3207
+ const explicitLabel = content.match(explicitLabelRegex);
3208
+ if (explicitLabel && explicitLabel[1].trim()) {
3209
+ hasValidLabel = true;
3210
+ labelMethods.push('explicit label');
3211
+ } else {
3212
+ issues.push({
3213
+ type: '๐Ÿ“‹ Missing explicit label',
3214
+ description: `${elementType} ${index} with id="${id}" does not have an explicit <label for="${id}">`,
3215
+ element: element.substring(0, 100) + '...'
3216
+ });
3217
+ }
3218
+ }
3219
+
3220
+ // Check for implicit label (wrapped in label)
3221
+ const elementPosition = content.indexOf(element);
3222
+ if (elementPosition !== -1) {
3223
+ const beforeElement = content.substring(0, elementPosition);
3224
+ const afterElement = content.substring(elementPosition + element.length);
3225
+
3226
+ // Look for wrapping label
3227
+ const labelOpenRegex = /<label[^>]*>(?:[^<]*<[^>]*>)*[^<]*$/i;
3228
+ const labelCloseRegex = /^[^<]*(?:<[^>]*>[^<]*)*<\/label>/i;
3229
+
3230
+ const hasOpenLabel = labelOpenRegex.test(beforeElement);
3231
+ const hasCloseLabel = labelCloseRegex.test(afterElement);
3232
+
3233
+ if (hasOpenLabel && hasCloseLabel) {
3234
+ // Extract label text
3235
+ const labelMatch = beforeElement.match(/<label[^>]*>([^<]*)$/i);
3236
+ const labelText = labelMatch ? labelMatch[1].trim() : '';
3237
+
3238
+ if (labelText) {
3239
+ hasValidLabel = true;
3240
+ labelMethods.push('implicit label');
3241
+ } else {
3242
+ issues.push({
3243
+ type: '๐Ÿ“‹ Empty implicit label',
3244
+ description: `${elementType} ${index} is wrapped in <label> but label text is empty`,
3245
+ element: element.substring(0, 100) + '...'
3246
+ });
3247
+ }
3248
+ } else {
3249
+ issues.push({
3250
+ type: '๐Ÿ“‹ Missing implicit label',
3251
+ description: `${elementType} ${index} does not have an implicit (wrapped) <label>`,
3252
+ element: element.substring(0, 100) + '...'
3253
+ });
3254
+ }
3255
+ }
3256
+
3257
+ // Check aria-label
3258
+ if (ariaLabel && ariaLabel.trim()) {
3259
+ hasValidLabel = true;
3260
+ labelMethods.push('aria-label');
3261
+ } else {
3262
+ issues.push({
3263
+ type: '๐Ÿ“‹ Missing aria-label',
3264
+ description: `${elementType} ${index} aria-label attribute does not exist or is empty`,
3265
+ element: element.substring(0, 100) + '...'
3266
+ });
3267
+ }
3268
+
3269
+ // Check aria-labelledby
3270
+ if (ariaLabelledby) {
3271
+ const referencedIds = ariaLabelledby.split(/\s+/);
3272
+ let validReferences = 0;
3273
+
3274
+ referencedIds.forEach(refId => {
3275
+ if (refId.trim()) {
3276
+ const referencedElement = content.match(new RegExp(`<[^>]*id\\s*=\\s*["']${refId}["'][^>]*>([^<]*)</[^>]*>`, 'i'));
3277
+ if (referencedElement && referencedElement[1].trim()) {
3278
+ validReferences++;
3279
+ }
3280
+ }
3281
+ });
3282
+
3283
+ if (validReferences > 0) {
3284
+ hasValidLabel = true;
3285
+ labelMethods.push('aria-labelledby');
3286
+ } else {
3287
+ issues.push({
3288
+ type: '๐Ÿ“‹ Invalid aria-labelledby',
3289
+ description: `${elementType} ${index} aria-labelledby references elements that do not exist or are empty`,
3290
+ element: element.substring(0, 100) + '...'
3291
+ });
3292
+ }
3293
+ } else {
3294
+ issues.push({
3295
+ type: '๐Ÿ“‹ Missing aria-labelledby',
3296
+ description: `${elementType} ${index} aria-labelledby attribute does not exist`,
3297
+ element: element.substring(0, 100) + '...'
3298
+ });
3299
+ }
3300
+
3301
+ // Check title attribute
3302
+ if (!title || !title.trim()) {
3303
+ issues.push({
3304
+ type: '๐Ÿ“‹ Missing title',
3305
+ description: `${elementType} ${index} has no title attribute`,
3306
+ element: element.substring(0, 100) + '...'
3307
+ });
3308
+ }
3309
+
3310
+ // Check if element needs role="none" or role="presentation" to override default semantics
3311
+ const hasRole = /role\s*=/i.test(element);
3312
+ if (!hasRole && !hasValidLabel) {
3313
+ issues.push({
3314
+ type: '๐Ÿ“‹ Missing role override',
3315
+ description: `${elementType} ${index} default semantics were not overridden with role="none" or role="presentation"`,
3316
+ element: element.substring(0, 100) + '...'
3317
+ });
3318
+ }
3319
+
3320
+ return issues;
3321
+ }
3322
+
3323
+ fixFormLabelsInContent(content) {
3324
+ let fixed = content;
3325
+
3326
+ // Fix input elements
3327
+ const inputPatterns = [
3328
+ /<input[^>]*type\s*=\s*["'](?:text|email|password|tel|url|search|number|date|time|datetime-local|month|week|color|range|file)["'][^>]*>/gi,
3329
+ /<textarea[^>]*>/gi,
3330
+ /<select[^>]*>/gi
3331
+ ];
3332
+
3333
+ inputPatterns.forEach(pattern => {
3334
+ fixed = fixed.replace(pattern, (match) => {
3335
+ return this.addFormElementLabeling(match, fixed);
3336
+ });
3337
+ });
3338
+
3339
+ return fixed;
3340
+ }
3341
+
3342
+ addFormElementLabeling(element, content) {
3343
+ let enhanced = element;
3344
+
3345
+ // Extract current attributes
3346
+ const id = this.extractAttributeValue(element, 'id');
3347
+ const name = this.extractAttributeValue(element, 'name');
3348
+ const ariaLabel = this.extractAttributeValue(element, 'aria-label');
3349
+ const title = this.extractAttributeValue(element, 'title');
3350
+ const placeholder = this.extractAttributeValue(element, 'placeholder');
3351
+
3352
+ // Generate appropriate label text
3353
+ let labelText = this.generateFormLabelText(element, name, placeholder);
3354
+
3355
+ // Add aria-label if missing
3356
+ if (!ariaLabel && labelText) {
3357
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 aria-label="${labelText}"$2`);
3358
+ console.log(chalk.yellow(` ๐Ÿ“‹ Added aria-label="${labelText}" to form element`));
3359
+ }
3360
+
3361
+ // Add title if missing
3362
+ if (!title && labelText) {
3363
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 title="${labelText}"$2`);
3364
+ console.log(chalk.yellow(` ๐Ÿ“‹ Added title="${labelText}" to form element`));
3365
+ }
3366
+
3367
+ // Add id if missing (for potential explicit labeling)
3368
+ if (!id) {
3369
+ const generatedId = this.generateFormElementId(element, name);
3370
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 id="${generatedId}"$2`);
3371
+ console.log(chalk.yellow(` ๐Ÿ“‹ Added id="${generatedId}" to form element`));
3372
+ }
3373
+
3374
+ return enhanced;
3375
+ }
3376
+
3377
+ generateFormLabelText(element, name, placeholder) {
3378
+ const lang = this.config.language;
3379
+
3380
+ // Try to extract meaningful text from various sources
3381
+ if (placeholder && placeholder.trim()) {
3382
+ return placeholder.trim();
3383
+ }
3384
+
3385
+ if (name && name.trim()) {
3386
+ // Convert name to readable text
3387
+ const readable = name.replace(/[-_]/g, ' ')
3388
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
3389
+ .toLowerCase();
3390
+
3391
+ // Capitalize first letter
3392
+ return readable.charAt(0).toUpperCase() + readable.slice(1);
3393
+ }
3394
+
3395
+ // Extract input type for generic labels
3396
+ const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
3397
+ const inputType = typeMatch ? typeMatch[1] : 'text';
3398
+
3399
+ // Generate type-specific labels
3400
+ const typeLabels = {
3401
+ ja: {
3402
+ text: 'ใƒ†ใ‚ญใ‚นใƒˆๅ…ฅๅŠ›',
3403
+ email: 'ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚น',
3404
+ password: 'ใƒ‘ใ‚นใƒฏใƒผใƒ‰',
3405
+ tel: '้›ป่ฉฑ็•ชๅท',
3406
+ url: 'URL',
3407
+ search: 'ๆคœ็ดข',
3408
+ number: 'ๆ•ฐๅ€ค',
3409
+ date: 'ๆ—ฅไป˜',
3410
+ time: 'ๆ™‚ๅˆป',
3411
+ file: 'ใƒ•ใ‚กใ‚คใƒซ้ธๆŠž',
3412
+ textarea: 'ใƒ†ใ‚ญใ‚นใƒˆใ‚จใƒชใ‚ข',
3413
+ select: '้ธๆŠž'
3414
+ },
3415
+ en: {
3416
+ text: 'Text input',
3417
+ email: 'Email address',
3418
+ password: 'Password',
3419
+ tel: 'Phone number',
3420
+ url: 'URL',
3421
+ search: 'Search',
3422
+ number: 'Number',
3423
+ date: 'Date',
3424
+ time: 'Time',
3425
+ file: 'File selection',
3426
+ textarea: 'Text area',
3427
+ select: 'Selection'
3428
+ },
3429
+ vi: {
3430
+ text: 'Nhแบญp vฤƒn bแบฃn',
3431
+ email: 'ฤแป‹a chแป‰ email',
3432
+ password: 'Mแบญt khแบฉu',
3433
+ tel: 'Sแป‘ ฤ‘iแป‡n thoแบกi',
3434
+ url: 'URL',
3435
+ search: 'Tรฌm kiแบฟm',
3436
+ number: 'Sแป‘',
3437
+ date: 'Ngร y',
3438
+ time: 'Thแปi gian',
3439
+ file: 'Chแปn file',
3440
+ textarea: 'Vรนng vฤƒn bแบฃn',
3441
+ select: 'Lแปฑa chแปn'
3442
+ }
3443
+ };
3444
+
3445
+ const labels = typeLabels[lang] || typeLabels.en;
3446
+
3447
+ // Determine element type
3448
+ let elementType = inputType;
3449
+ if (element.includes('<textarea')) elementType = 'textarea';
3450
+ if (element.includes('<select')) elementType = 'select';
3451
+
3452
+ return labels[elementType] || labels.text;
3453
+ }
3454
+
3455
+ generateFormElementId(element, name) {
3456
+ if (name) {
3457
+ return `${name}_input`;
3458
+ }
3459
+
3460
+ // Generate based on type
3461
+ const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
3462
+ const inputType = typeMatch ? typeMatch[1] : 'text';
3463
+
3464
+ const timestamp = Date.now().toString().slice(-6);
3465
+ return `${inputType}_${timestamp}`;
3466
+ }
3467
+
3468
+ extractAttributeValue(element, attributeName) {
3469
+ const regex = new RegExp(`${attributeName}\\s*=\\s*["']([^"']*)["']`, 'i');
3470
+ const match = element.match(regex);
3471
+ return match ? match[1] : null;
3472
+ }
3473
+
3109
3474
  analyzeHeadingStructure(content) {
3110
3475
  const issues = [];
3111
3476
 
@@ -3177,6 +3542,918 @@ class AccessibilityFixer {
3177
3542
  return issues;
3178
3543
  }
3179
3544
 
3545
+ async fixAllAccessibilityIssues(directory = '.') {
3546
+ console.log(chalk.blue('๐Ÿš€ Starting comprehensive accessibility fixes...'));
3547
+ console.log('');
3548
+
3549
+ const results = {
3550
+ totalFiles: 0,
3551
+ fixedFiles: 0,
3552
+ totalIssues: 0,
3553
+ steps: []
3554
+ };
3555
+
3556
+ try {
3557
+ // Step 1: HTML lang attributes
3558
+ console.log(chalk.blue('๐Ÿ“ Step 1: HTML lang attributes...'));
3559
+ const langResults = await this.fixHtmlLang(directory);
3560
+ const langFixed = langResults.filter(r => r.status === 'fixed').length;
3561
+ results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
3562
+
3563
+ // Step 2: Alt attributes
3564
+ console.log(chalk.blue('๐Ÿ–ผ๏ธ Step 2: Alt attributes...'));
3565
+ const altResults = await this.fixEmptyAltAttributes(directory);
3566
+ const altFixed = altResults.filter(r => r.status === 'fixed').length;
3567
+ const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3568
+ results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
3569
+
3570
+ // Step 3: Role attributes
3571
+ console.log(chalk.blue('๐ŸŽญ Step 3: Role attributes...'));
3572
+ const roleResults = await this.fixRoleAttributes(directory);
3573
+ const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
3574
+ const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3575
+ results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
3576
+
3577
+ // Step 4: Form labels
3578
+ console.log(chalk.blue('๐Ÿ“‹ Step 4: Form labels...'));
3579
+ const formResults = await this.fixFormLabels(directory);
3580
+ const formFixed = formResults.filter(r => r.status === 'fixed').length;
3581
+ const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3582
+ results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
3583
+
3584
+ // Step 5: Button names
3585
+ console.log(chalk.blue('๐Ÿ”˜ Step 5: Button names...'));
3586
+ const buttonResults = await this.fixButtonNames(directory);
3587
+ const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
3588
+ const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3589
+ results.steps.push({ step: 5, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
3590
+
3591
+ // Step 6: Link names
3592
+ console.log(chalk.blue('๐Ÿ”— Step 6: Link names...'));
3593
+ const linkResults = await this.fixLinkNames(directory);
3594
+ const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
3595
+ const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3596
+ results.steps.push({ step: 6, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
3597
+
3598
+ // Step 7: Landmarks
3599
+ console.log(chalk.blue('๐Ÿ›๏ธ Step 7: Landmarks...'));
3600
+ const landmarkResults = await this.fixLandmarks(directory);
3601
+ const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
3602
+ const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3603
+ results.steps.push({ step: 7, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
3604
+
3605
+ // Step 8: Heading analysis
3606
+ console.log(chalk.blue('๐Ÿ“‘ Step 8: Heading analysis...'));
3607
+ const headingResults = await this.analyzeHeadings(directory);
3608
+ const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3609
+ results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
3610
+ console.log(chalk.gray('๐Ÿ’ก Heading issues require manual review and cannot be auto-fixed'));
3611
+
3612
+ // Step 9: Broken links check
3613
+ console.log(chalk.blue('๐Ÿ”— Step 9: Broken links check...'));
3614
+ const brokenLinksResults = await this.checkBrokenLinks(directory);
3615
+ const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3616
+ results.steps.push({ step: 9, name: 'Broken links check', issues: totalBrokenLinks });
3617
+ console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
3618
+
3619
+ // Step 10: Cleanup duplicate roles
3620
+ console.log(chalk.blue('๐Ÿงน Step 10: Cleanup duplicate roles...'));
3621
+ const cleanupResults = await this.cleanupDuplicateRoles(directory);
3622
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
3623
+ results.steps.push({ step: 10, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
3624
+
3625
+ // Calculate totals
3626
+ results.totalFiles = Math.max(
3627
+ langResults.length, altResults.length, roleResults.length, formResults.length,
3628
+ buttonResults.length, linkResults.length, landmarkResults.length,
3629
+ headingResults.length, brokenLinksResults.length, cleanupResults.length
3630
+ );
3631
+
3632
+ results.fixedFiles = new Set([
3633
+ ...langResults.filter(r => r.status === 'fixed').map(r => r.file),
3634
+ ...altResults.filter(r => r.status === 'fixed').map(r => r.file),
3635
+ ...roleResults.filter(r => r.status === 'fixed').map(r => r.file),
3636
+ ...formResults.filter(r => r.status === 'fixed').map(r => r.file),
3637
+ ...buttonResults.filter(r => r.status === 'fixed').map(r => r.file),
3638
+ ...linkResults.filter(r => r.status === 'fixed').map(r => r.file),
3639
+ ...landmarkResults.filter(r => r.status === 'fixed').map(r => r.file),
3640
+ ...cleanupResults.filter(r => r.status === 'fixed').map(r => r.file)
3641
+ ]).size;
3642
+
3643
+ results.totalIssues = totalAltIssues + totalRoleIssues + totalFormIssues +
3644
+ totalButtonIssues + totalLinkIssues + totalLandmarkIssues;
3645
+
3646
+ // Final summary
3647
+ console.log(chalk.green('\n๐ŸŽ‰ All accessibility fixes completed!'));
3648
+ console.log(chalk.blue('๐Ÿ“Š Final Summary:'));
3649
+ console.log(chalk.blue(` Total files scanned: ${results.totalFiles}`));
3650
+ console.log(chalk.blue(` Files fixed: ${results.fixedFiles}`));
3651
+ console.log(chalk.blue(` Total issues resolved: ${results.totalIssues}`));
3652
+
3653
+ if (this.config.dryRun) {
3654
+ console.log(chalk.yellow('\n๐Ÿ’ก This was a dry run. Use without --dry-run to apply changes.'));
3655
+ }
3656
+
3657
+ return results;
3658
+
3659
+ } catch (error) {
3660
+ console.error(chalk.red(`โŒ Error during comprehensive fixes: ${error.message}`));
3661
+ throw error;
3662
+ }
3663
+ }
3664
+
3665
+ async fixButtonNames(directory = '.') {
3666
+ console.log(chalk.blue('๐Ÿ”˜ Fixing button names...'));
3667
+
3668
+ const htmlFiles = await this.findHtmlFiles(directory);
3669
+ const results = [];
3670
+ let totalIssuesFound = 0;
3671
+
3672
+ for (const file of htmlFiles) {
3673
+ try {
3674
+ const content = await fs.readFile(file, 'utf8');
3675
+ const issues = this.analyzeButtonNames(content);
3676
+
3677
+ if (issues.length > 0) {
3678
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3679
+ issues.forEach(issue => {
3680
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3681
+ totalIssuesFound++;
3682
+ });
3683
+ }
3684
+
3685
+ const fixed = this.fixButtonNamesInContent(content);
3686
+
3687
+ if (fixed !== content) {
3688
+ if (this.config.backupFiles) {
3689
+ await fs.writeFile(`${file}.backup`, content);
3690
+ }
3691
+
3692
+ if (!this.config.dryRun) {
3693
+ await fs.writeFile(file, fixed);
3694
+ }
3695
+
3696
+ console.log(chalk.green(`โœ… Fixed button names in: ${file}`));
3697
+ results.push({ file, status: 'fixed', issues: issues.length });
3698
+ } else {
3699
+ results.push({ file, status: 'no-change', issues: issues.length });
3700
+ }
3701
+ } catch (error) {
3702
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
3703
+ results.push({ file, status: 'error', error: error.message });
3704
+ }
3705
+ }
3706
+
3707
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} button name issues across ${results.length} files`));
3708
+ return results;
3709
+ }
3710
+
3711
+ analyzeButtonNames(content) {
3712
+ const issues = [];
3713
+ const buttonPattern = /<button[^>]*>[\s\S]*?<\/button>/gi;
3714
+ const buttons = content.match(buttonPattern) || [];
3715
+
3716
+ buttons.forEach((button, index) => {
3717
+ const buttonText = button.replace(/<[^>]*>/g, '').trim();
3718
+ const hasAriaLabel = /aria-label\s*=/i.test(button);
3719
+ const hasTitle = /title\s*=/i.test(button);
3720
+
3721
+ if (!buttonText && !hasAriaLabel && !hasTitle) {
3722
+ issues.push({
3723
+ type: '๐Ÿ”˜ Empty button',
3724
+ description: `Button ${index + 1} has no text content, aria-label, or title`,
3725
+ element: button.substring(0, 100) + '...'
3726
+ });
3727
+ }
3728
+ });
3729
+
3730
+ return issues;
3731
+ }
3732
+
3733
+ fixButtonNamesInContent(content) {
3734
+ let fixed = content;
3735
+
3736
+ const buttonPattern = /<button([^>]*)>([\s\S]*?)<\/button>/gi;
3737
+
3738
+ fixed = fixed.replace(buttonPattern, (match, attributes, innerContent) => {
3739
+ const buttonText = innerContent.replace(/<[^>]*>/g, '').trim();
3740
+ const hasAriaLabel = /aria-label\s*=/i.test(attributes);
3741
+ const hasTitle = /title\s*=/i.test(attributes);
3742
+
3743
+ if (!buttonText && !hasAriaLabel && !hasTitle) {
3744
+ const buttonName = this.generateButtonName(attributes, innerContent);
3745
+ const updatedAttributes = attributes + ` aria-label="${buttonName}" title="${buttonName}"`;
3746
+ console.log(chalk.yellow(` ๐Ÿ”˜ Added aria-label and title to empty button: "${buttonName}"`));
3747
+ return `<button${updatedAttributes}>${innerContent}</button>`;
3748
+ }
3749
+
3750
+ return match;
3751
+ });
3752
+
3753
+ return fixed;
3754
+ }
3755
+
3756
+ generateButtonName(attributes, innerContent) {
3757
+ const lang = this.config.language;
3758
+
3759
+ // Try to extract meaningful name from onclick or other attributes
3760
+ const onclickMatch = attributes.match(/onclick\s*=\s*["']([^"']+)["']/i);
3761
+ if (onclickMatch) {
3762
+ const onclick = onclickMatch[1];
3763
+ if (onclick.includes('submit')) return lang === 'ja' ? '้€ไฟก' : 'Submit';
3764
+ if (onclick.includes('cancel')) return lang === 'ja' ? 'ใ‚ญใƒฃใƒณใ‚ปใƒซ' : 'Cancel';
3765
+ if (onclick.includes('close')) return lang === 'ja' ? '้–‰ใ˜ใ‚‹' : 'Close';
3766
+ if (onclick.includes('save')) return lang === 'ja' ? 'ไฟๅญ˜' : 'Save';
3767
+ }
3768
+
3769
+ // Check for common class names
3770
+ const classMatch = attributes.match(/class\s*=\s*["']([^"']+)["']/i);
3771
+ if (classMatch) {
3772
+ const className = classMatch[1].toLowerCase();
3773
+ if (className.includes('submit')) return lang === 'ja' ? '้€ไฟก' : 'Submit';
3774
+ if (className.includes('cancel')) return lang === 'ja' ? 'ใ‚ญใƒฃใƒณใ‚ปใƒซ' : 'Cancel';
3775
+ if (className.includes('close')) return lang === 'ja' ? '้–‰ใ˜ใ‚‹' : 'Close';
3776
+ if (className.includes('save')) return lang === 'ja' ? 'ไฟๅญ˜' : 'Save';
3777
+ }
3778
+
3779
+ return lang === 'ja' ? 'ใƒœใ‚ฟใƒณ' : 'Button';
3780
+ }
3781
+
3782
+ async fixLinkNames(directory = '.') {
3783
+ console.log(chalk.blue('๐Ÿ”— Fixing link names...'));
3784
+
3785
+ const htmlFiles = await this.findHtmlFiles(directory);
3786
+ const results = [];
3787
+ let totalIssuesFound = 0;
3788
+
3789
+ for (const file of htmlFiles) {
3790
+ try {
3791
+ const content = await fs.readFile(file, 'utf8');
3792
+ const issues = this.analyzeLinkNames(content);
3793
+
3794
+ if (issues.length > 0) {
3795
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3796
+ issues.forEach(issue => {
3797
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3798
+ totalIssuesFound++;
3799
+ });
3800
+ }
3801
+
3802
+ const fixed = this.fixLinkNamesInContent(content);
3803
+
3804
+ if (fixed !== content) {
3805
+ if (this.config.backupFiles) {
3806
+ await fs.writeFile(`${file}.backup`, content);
3807
+ }
3808
+
3809
+ if (!this.config.dryRun) {
3810
+ await fs.writeFile(file, fixed);
3811
+ }
3812
+
3813
+ console.log(chalk.green(`โœ… Fixed link names in: ${file}`));
3814
+ results.push({ file, status: 'fixed', issues: issues.length });
3815
+ } else {
3816
+ results.push({ file, status: 'no-change', issues: issues.length });
3817
+ }
3818
+ } catch (error) {
3819
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
3820
+ results.push({ file, status: 'error', error: error.message });
3821
+ }
3822
+ }
3823
+
3824
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} link name issues across ${results.length} files`));
3825
+ return results;
3826
+ }
3827
+
3828
+ analyzeLinkNames(content) {
3829
+ const issues = [];
3830
+ const linkPattern = /<a[^>]*href[^>]*>[\s\S]*?<\/a>/gi;
3831
+ const links = content.match(linkPattern) || [];
3832
+
3833
+ links.forEach((link, index) => {
3834
+ const linkText = link.replace(/<[^>]*>/g, '').trim();
3835
+ const hasAriaLabel = /aria-label\s*=/i.test(link);
3836
+ const hasTitle = /title\s*=/i.test(link);
3837
+
3838
+ if (!linkText && !hasAriaLabel && !hasTitle) {
3839
+ issues.push({
3840
+ type: '๐Ÿ”— Empty link',
3841
+ description: `Link ${index + 1} has no text content, aria-label, or title`,
3842
+ element: link.substring(0, 100) + '...'
3843
+ });
3844
+ }
3845
+
3846
+ // Check for generic link text
3847
+ const genericTexts = ['click here', 'read more', 'more', 'here', 'link'];
3848
+ if (genericTexts.some(generic => linkText.toLowerCase().includes(generic))) {
3849
+ issues.push({
3850
+ type: '๐Ÿ”— Generic link text',
3851
+ description: `Link ${index + 1} has generic text: "${linkText}"`,
3852
+ element: link.substring(0, 100) + '...'
3853
+ });
3854
+ }
3855
+ });
3856
+
3857
+ return issues;
3858
+ }
3859
+
3860
+ fixLinkNamesInContent(content) {
3861
+ let fixed = content;
3862
+
3863
+ const linkPattern = /<a([^>]*href[^>]*?)>([\s\S]*?)<\/a>/gi;
3864
+
3865
+ fixed = fixed.replace(linkPattern, (match, attributes, innerContent) => {
3866
+ const linkText = innerContent.replace(/<[^>]*>/g, '').trim();
3867
+ const hasAriaLabel = /aria-label\s*=/i.test(attributes);
3868
+ const hasTitle = /title\s*=/i.test(attributes);
3869
+
3870
+ if (!linkText && !hasAriaLabel && !hasTitle) {
3871
+ const linkName = this.generateLinkName(attributes, innerContent);
3872
+ const updatedAttributes = attributes + ` aria-label="${linkName}" title="${linkName}"`;
3873
+ console.log(chalk.yellow(` ๐Ÿ”— Added aria-label and title to empty link: "${linkName}"`));
3874
+ return `<a${updatedAttributes}>${innerContent}</a>`;
3875
+ }
3876
+
3877
+ return match;
3878
+ });
3879
+
3880
+ return fixed;
3881
+ }
3882
+
3883
+ generateLinkName(attributes, innerContent) {
3884
+ const lang = this.config.language;
3885
+
3886
+ // Try to extract href for context
3887
+ const hrefMatch = attributes.match(/href\s*=\s*["']([^"']+)["']/i);
3888
+ if (hrefMatch) {
3889
+ const href = hrefMatch[1];
3890
+ if (href.includes('mailto:')) return lang === 'ja' ? 'ใƒกใƒผใƒซ้€ไฟก' : 'Send email';
3891
+ if (href.includes('tel:')) return lang === 'ja' ? '้›ป่ฉฑใ‚’ใ‹ใ‘ใ‚‹' : 'Make call';
3892
+ if (href.includes('#')) return lang === 'ja' ? 'ใƒšใƒผใ‚ธๅ†…ใƒชใƒณใ‚ฏ' : 'Page anchor';
3893
+ if (href.includes('.pdf')) return lang === 'ja' ? 'PDFใ‚’้–‹ใ' : 'Open PDF';
3894
+ }
3895
+
3896
+ return lang === 'ja' ? 'ใƒชใƒณใ‚ฏ' : 'Link';
3897
+ }
3898
+
3899
+ async fixLandmarks(directory = '.') {
3900
+ console.log(chalk.blue('๐Ÿ›๏ธ Fixing landmarks...'));
3901
+
3902
+ const htmlFiles = await this.findHtmlFiles(directory);
3903
+ const results = [];
3904
+ let totalIssuesFound = 0;
3905
+
3906
+ for (const file of htmlFiles) {
3907
+ try {
3908
+ const content = await fs.readFile(file, 'utf8');
3909
+ const issues = this.analyzeLandmarks(content);
3910
+
3911
+ if (issues.length > 0) {
3912
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3913
+ issues.forEach(issue => {
3914
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3915
+ totalIssuesFound++;
3916
+ });
3917
+ }
3918
+
3919
+ results.push({ file, status: 'no-change', issues: issues.length });
3920
+ } catch (error) {
3921
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
3922
+ results.push({ file, status: 'error', error: error.message });
3923
+ }
3924
+ }
3925
+
3926
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} landmark issues across ${results.length} files`));
3927
+ return results;
3928
+ }
3929
+
3930
+ analyzeLandmarks(content) {
3931
+ const issues = [];
3932
+
3933
+ const hasMain = /<main[^>]*>/i.test(content);
3934
+ const hasNav = /<nav[^>]*>/i.test(content);
3935
+ const hasHeader = /<header[^>]*>/i.test(content);
3936
+ const hasFooter = /<footer[^>]*>/i.test(content);
3937
+
3938
+ if (!hasMain) {
3939
+ issues.push({
3940
+ type: '๐Ÿ›๏ธ Missing main landmark',
3941
+ description: 'Page should have a main landmark',
3942
+ suggestion: 'Add <main> element around primary content'
3943
+ });
3944
+ }
3945
+
3946
+ return issues;
3947
+ }
3948
+
3949
+ async analyzeHeadings(directory = '.') {
3950
+ console.log(chalk.blue('๐Ÿ“‘ Analyzing heading structure...'));
3951
+
3952
+ const htmlFiles = await this.findHtmlFiles(directory);
3953
+ const results = [];
3954
+ let totalIssuesFound = 0;
3955
+
3956
+ for (const file of htmlFiles) {
3957
+ try {
3958
+ const content = await fs.readFile(file, 'utf8');
3959
+ const issues = this.analyzeHeadingStructure(content);
3960
+
3961
+ if (issues.length > 0) {
3962
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3963
+ issues.forEach(issue => {
3964
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3965
+ if (issue.suggestion) {
3966
+ console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
3967
+ }
3968
+ totalIssuesFound++;
3969
+ });
3970
+ }
3971
+
3972
+ results.push({ file, status: 'analyzed', issues: issues.length });
3973
+ } catch (error) {
3974
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
3975
+ results.push({ file, status: 'error', error: error.message });
3976
+ }
3977
+ }
3978
+
3979
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed heading structure in ${results.length} files`));
3980
+ console.log(chalk.gray('๐Ÿ’ก Heading issues require manual review and cannot be auto-fixed'));
3981
+ return results;
3982
+ }
3983
+
3984
+ async checkBrokenLinks(directory = '.') {
3985
+ console.log(chalk.blue('๐Ÿ”— Checking for broken links and 404 resources...'));
3986
+
3987
+ const htmlFiles = await this.findHtmlFiles(directory);
3988
+ const results = [];
3989
+ let totalIssuesFound = 0;
3990
+
3991
+ for (const file of htmlFiles) {
3992
+ try {
3993
+ const content = await fs.readFile(file, 'utf8');
3994
+ const issues = this.analyzeBrokenLinks(content, file);
3995
+
3996
+ if (issues.length > 0) {
3997
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
3998
+ issues.forEach(issue => {
3999
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4000
+ if (issue.suggestion) {
4001
+ console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
4002
+ }
4003
+ totalIssuesFound++;
4004
+ });
4005
+ }
4006
+
4007
+ results.push({ file, status: 'analyzed', issues: issues.length });
4008
+ } catch (error) {
4009
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
4010
+ results.push({ file, status: 'error', error: error.message });
4011
+ }
4012
+ }
4013
+
4014
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Analyzed links in ${results.length} files`));
4015
+ console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
4016
+ return results;
4017
+ }
4018
+
4019
+ analyzeBrokenLinks(content, filePath) {
4020
+ const issues = [];
4021
+
4022
+ // Check for local image files
4023
+ const imgPattern = /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
4024
+ let match;
4025
+
4026
+ while ((match = imgPattern.exec(content)) !== null) {
4027
+ const src = match[1];
4028
+
4029
+ // Skip external URLs and data URLs
4030
+ if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('//')) {
4031
+ continue;
4032
+ }
4033
+
4034
+ // Check if local file exists
4035
+ const fullPath = path.resolve(path.dirname(filePath), src);
4036
+ try {
4037
+ require('fs').statSync(fullPath);
4038
+ } catch (error) {
4039
+ issues.push({
4040
+ type: '๐Ÿ“ Image not found',
4041
+ description: `img file does not exist: ${src}`,
4042
+ suggestion: 'Create the missing file or update the image path'
4043
+ });
4044
+ }
4045
+ }
4046
+
4047
+ return issues;
4048
+ }
4049
+
4050
+ async cleanupDuplicateRoles(directory = '.') {
4051
+ console.log(chalk.blue('๐Ÿงน Cleaning up duplicate role attributes...'));
4052
+
4053
+ const htmlFiles = await this.findHtmlFiles(directory);
4054
+ const results = [];
4055
+ let totalIssuesFound = 0;
4056
+
4057
+ for (const file of htmlFiles) {
4058
+ try {
4059
+ const content = await fs.readFile(file, 'utf8');
4060
+ const fixed = this.cleanupDuplicateRolesInContent(content);
4061
+
4062
+ if (fixed !== content) {
4063
+ if (this.config.backupFiles) {
4064
+ await fs.writeFile(`${file}.backup`, content);
4065
+ }
4066
+
4067
+ if (!this.config.dryRun) {
4068
+ await fs.writeFile(file, fixed);
4069
+ }
4070
+
4071
+ console.log(chalk.green(`โœ… Cleaned duplicate roles in: ${file}`));
4072
+ results.push({ file, status: 'fixed' });
4073
+ totalIssuesFound++;
4074
+ } else {
4075
+ results.push({ file, status: 'no-change' });
4076
+ }
4077
+ } catch (error) {
4078
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
4079
+ results.push({ file, status: 'error', error: error.message });
4080
+ }
4081
+ }
4082
+
4083
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Cleaned duplicate roles in ${totalIssuesFound} files`));
4084
+ return results;
4085
+ }
4086
+
4087
+ cleanupDuplicateRolesInContent(content) {
4088
+ let fixed = content;
4089
+
4090
+ // Remove duplicate role attributes
4091
+ fixed = fixed.replace(/(\s+role\s*=\s*["'][^"']*["'])\s+role\s*=\s*["'][^"']*["']/gi, '$1');
4092
+
4093
+ return fixed;
4094
+ }
4095
+
4096
+ async fixNestedInteractiveControls(directory = '.') {
4097
+ console.log(chalk.blue('๐ŸŽฏ Fixing nested interactive controls...'));
4098
+
4099
+ const htmlFiles = await this.findHtmlFiles(directory);
4100
+ const results = [];
4101
+ let totalIssuesFound = 0;
4102
+
4103
+ for (const file of htmlFiles) {
4104
+ try {
4105
+ const content = await fs.readFile(file, 'utf8');
4106
+ const issues = this.analyzeNestedInteractiveControls(content);
4107
+
4108
+ if (issues.length > 0) {
4109
+ console.log(chalk.cyan(`\n๐Ÿ“ ${file}:`));
4110
+ issues.forEach(issue => {
4111
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4112
+ if (issue.suggestion) {
4113
+ console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`));
4114
+ }
4115
+ totalIssuesFound++;
4116
+ });
4117
+ }
4118
+
4119
+ const fixed = this.fixNestedInteractiveControlsInContent(content);
4120
+
4121
+ if (fixed !== content) {
4122
+ if (this.config.backupFiles) {
4123
+ await fs.writeFile(`${file}.backup`, content);
4124
+ }
4125
+
4126
+ if (!this.config.dryRun) {
4127
+ await fs.writeFile(file, fixed);
4128
+ }
4129
+
4130
+ console.log(chalk.green(`โœ… Fixed nested interactive controls in: ${file}`));
4131
+ results.push({ file, status: 'fixed', issues: issues.length });
4132
+ } else {
4133
+ results.push({ file, status: 'no-change', issues: issues.length });
4134
+ }
4135
+ } catch (error) {
4136
+ console.error(chalk.red(`โŒ Error processing ${file}: ${error.message}`));
4137
+ results.push({ file, status: 'error', error: error.message });
4138
+ }
4139
+ }
4140
+
4141
+ console.log(chalk.blue(`\n๐Ÿ“Š Summary: Found ${totalIssuesFound} nested interactive control issues across ${results.length} files`));
4142
+ return results;
4143
+ }
4144
+
4145
+ analyzeNestedInteractiveControls(content) {
4146
+ const issues = [];
4147
+
4148
+ // Define interactive elements and their roles
4149
+ const interactiveElements = [
4150
+ { tag: 'button', role: 'button' },
4151
+ { tag: 'a', role: 'link', requiresHref: true },
4152
+ { tag: 'input', role: 'textbox|button|checkbox|radio|slider|spinbutton' },
4153
+ { tag: 'textarea', role: 'textbox' },
4154
+ { tag: 'select', role: 'combobox|listbox' },
4155
+ { tag: 'details', role: 'group' },
4156
+ { tag: 'summary', role: 'button' }
4157
+ ];
4158
+
4159
+ // Also check for elements with interactive roles
4160
+ const interactiveRoles = [
4161
+ 'button', 'link', 'textbox', 'checkbox', 'radio', 'slider',
4162
+ 'spinbutton', 'combobox', 'listbox', 'menuitem', 'tab',
4163
+ 'treeitem', 'gridcell', 'option'
4164
+ ];
4165
+
4166
+ // Find all interactive elements
4167
+ const interactiveSelectors = [];
4168
+
4169
+ // Add tag-based selectors
4170
+ interactiveElements.forEach(element => {
4171
+ if (element.requiresHref) {
4172
+ interactiveSelectors.push(`<${element.tag}[^>]*href[^>]*>`);
4173
+ } else {
4174
+ interactiveSelectors.push(`<${element.tag}[^>]*>`);
4175
+ }
4176
+ });
4177
+
4178
+ // Add role-based selectors
4179
+ interactiveRoles.forEach(role => {
4180
+ interactiveSelectors.push(`<[^>]*role\\s*=\\s*["']${role}["'][^>]*>`);
4181
+ });
4182
+
4183
+ // Create combined regex pattern
4184
+ const interactivePattern = new RegExp(interactiveSelectors.join('|'), 'gi');
4185
+
4186
+ // Find all interactive elements with their positions
4187
+ const interactiveMatches = [];
4188
+ let match;
4189
+
4190
+ while ((match = interactivePattern.exec(content)) !== null) {
4191
+ const element = match[0];
4192
+ const startPos = match.index;
4193
+ const endPos = this.findElementEndPosition(content, element, startPos);
4194
+
4195
+ if (endPos > startPos) {
4196
+ interactiveMatches.push({
4197
+ element: element,
4198
+ startPos: startPos,
4199
+ endPos: endPos,
4200
+ fullElement: content.substring(startPos, endPos)
4201
+ });
4202
+ }
4203
+ }
4204
+
4205
+ // Check for nesting
4206
+ for (let i = 0; i < interactiveMatches.length; i++) {
4207
+ const parent = interactiveMatches[i];
4208
+
4209
+ for (let j = 0; j < interactiveMatches.length; j++) {
4210
+ if (i === j) continue;
4211
+
4212
+ const child = interactiveMatches[j];
4213
+
4214
+ // Check if child is nested inside parent
4215
+ if (child.startPos > parent.startPos && child.endPos < parent.endPos) {
4216
+ const parentType = this.getInteractiveElementType(parent.element);
4217
+ const childType = this.getInteractiveElementType(child.element);
4218
+
4219
+ issues.push({
4220
+ type: '๐ŸŽฏ Nested interactive controls',
4221
+ description: `${childType} is nested inside ${parentType}`,
4222
+ parentElement: parent.element.substring(0, 100) + '...',
4223
+ childElement: child.element.substring(0, 100) + '...',
4224
+ suggestion: `Remove interactive role from parent or child, or restructure HTML to avoid nesting`
4225
+ });
4226
+ }
4227
+ }
4228
+ }
4229
+
4230
+ return issues;
4231
+ }
4232
+
4233
+ findElementEndPosition(content, startTag, startPos) {
4234
+ // Extract tag name from start tag
4235
+ const tagMatch = startTag.match(/<(\w+)/);
4236
+ if (!tagMatch) return startPos + startTag.length;
4237
+
4238
+ const tagName = tagMatch[1].toLowerCase();
4239
+
4240
+ // Self-closing tags
4241
+ if (startTag.endsWith('/>') || ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tagName)) {
4242
+ return startPos + startTag.length;
4243
+ }
4244
+
4245
+ // Find matching closing tag
4246
+ const closeTagPattern = new RegExp(`</${tagName}>`, 'i');
4247
+ const remainingContent = content.substring(startPos + startTag.length);
4248
+ const closeMatch = remainingContent.match(closeTagPattern);
4249
+
4250
+ if (closeMatch) {
4251
+ return startPos + startTag.length + closeMatch.index + closeMatch[0].length;
4252
+ }
4253
+
4254
+ // If no closing tag found, assume it ends at the start tag
4255
+ return startPos + startTag.length;
4256
+ }
4257
+
4258
+ getInteractiveElementType(element) {
4259
+ // Extract tag name
4260
+ const tagMatch = element.match(/<(\w+)/);
4261
+ const tagName = tagMatch ? tagMatch[1].toLowerCase() : 'element';
4262
+
4263
+ // Extract role if present
4264
+ const roleMatch = element.match(/role\s*=\s*["']([^"']+)["']/i);
4265
+ const role = roleMatch ? roleMatch[1] : null;
4266
+
4267
+ if (role) {
4268
+ return `${tagName}[role="${role}"]`;
4269
+ }
4270
+
4271
+ // Special cases
4272
+ if (tagName === 'a' && /href\s*=/i.test(element)) {
4273
+ return 'link';
4274
+ }
4275
+
4276
+ if (tagName === 'input') {
4277
+ const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
4278
+ const inputType = typeMatch ? typeMatch[1] : 'text';
4279
+ return `input[type="${inputType}"]`;
4280
+ }
4281
+
4282
+ return tagName;
4283
+ }
4284
+
4285
+ fixNestedInteractiveControlsInContent(content) {
4286
+ let fixed = content;
4287
+
4288
+ // Strategy 1: Remove role attributes from parent containers that have interactive children
4289
+ const issues = this.analyzeNestedInteractiveControls(content);
4290
+
4291
+ issues.forEach(issue => {
4292
+ // Try to fix by removing role from parent element
4293
+ const parentRoleMatch = issue.parentElement.match(/role\s*=\s*["'][^"']*["']/i);
4294
+ if (parentRoleMatch) {
4295
+ const parentWithoutRole = issue.parentElement.replace(/\s*role\s*=\s*["'][^"']*["']/i, '');
4296
+ fixed = fixed.replace(issue.parentElement, parentWithoutRole);
4297
+ console.log(chalk.yellow(` ๐ŸŽฏ Removed role attribute from parent element to fix nesting`));
4298
+ }
4299
+ });
4300
+
4301
+ // Strategy 2: Convert div[role="button"] containing links to regular div
4302
+ fixed = fixed.replace(/<div([^>]*role\s*=\s*["']button["'][^>]*)>([\s\S]*?)<\/div>/gi, (match, attributes, content) => {
4303
+ // Check if content contains interactive elements
4304
+ const hasInteractiveChildren = /<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>/i.test(content);
4305
+
4306
+ if (hasInteractiveChildren) {
4307
+ // Remove role="button" and any button-related attributes
4308
+ const cleanAttributes = attributes
4309
+ .replace(/\s*role\s*=\s*["']button["']/i, '')
4310
+ .replace(/\s*tabindex\s*=\s*["'][^"']*["']/i, '')
4311
+ .replace(/\s*onclick\s*=\s*["'][^"']*["']/i, '');
4312
+
4313
+ console.log(chalk.yellow(` ๐ŸŽฏ Converted div[role="button"] to regular div due to interactive children`));
4314
+ return `<div${cleanAttributes}>${content}</div>`;
4315
+ }
4316
+
4317
+ return match;
4318
+ });
4319
+
4320
+ // Strategy 3: Remove tabindex from parent containers with interactive children
4321
+ fixed = fixed.replace(/(<[^>]+)(\s+tabindex\s*=\s*["'][^"']*["'])([^>]*>[\s\S]*?<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>[\s\S]*?<\/[^>]+>)/gi, (match, beforeTabindex, tabindexAttr, afterTabindex) => {
4322
+ console.log(chalk.yellow(` ๐ŸŽฏ Removed tabindex from parent element with interactive children`));
4323
+ return beforeTabindex + afterTabindex;
4324
+ });
4325
+
4326
+ return fixed;
4327
+ }
4328
+
4329
+ async fixAllAccessibilityIssues(directory = '.') {
4330
+ console.log(chalk.blue('๐Ÿš€ Starting comprehensive accessibility fixes...'));
4331
+ console.log('');
4332
+
4333
+ const results = {
4334
+ totalFiles: 0,
4335
+ fixedFiles: 0,
4336
+ totalIssues: 0,
4337
+ steps: []
4338
+ };
4339
+
4340
+ try {
4341
+ // Step 1: HTML lang attributes
4342
+ console.log(chalk.blue('๐Ÿ“ Step 1: HTML lang attributes...'));
4343
+ const langResults = await this.fixHtmlLang(directory);
4344
+ const langFixed = langResults.filter(r => r.status === 'fixed').length;
4345
+ results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
4346
+
4347
+ // Step 2: Alt attributes
4348
+ console.log(chalk.blue('๐Ÿ–ผ๏ธ Step 2: Alt attributes...'));
4349
+ const altResults = await this.fixEmptyAltAttributes(directory);
4350
+ const altFixed = altResults.filter(r => r.status === 'fixed').length;
4351
+ const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4352
+ results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
4353
+
4354
+ // Step 3: Role attributes
4355
+ console.log(chalk.blue('๐ŸŽญ Step 3: Role attributes...'));
4356
+ const roleResults = await this.fixRoleAttributes(directory);
4357
+ const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
4358
+ const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4359
+ results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
4360
+
4361
+ // Step 4: Form labels
4362
+ console.log(chalk.blue('๐Ÿ“‹ Step 4: Form labels...'));
4363
+ const formResults = await this.fixFormLabels(directory);
4364
+ const formFixed = formResults.filter(r => r.status === 'fixed').length;
4365
+ const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4366
+ results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
4367
+
4368
+ // Step 5: Nested interactive controls (NEW!)
4369
+ console.log(chalk.blue('๐ŸŽฏ Step 5: Nested interactive controls...'));
4370
+ const nestedResults = await this.fixNestedInteractiveControls(directory);
4371
+ const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
4372
+ const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4373
+ results.steps.push({ step: 5, name: 'Nested interactive controls', fixed: nestedFixed, issues: totalNestedIssues });
4374
+
4375
+ // Step 6: Button names
4376
+ console.log(chalk.blue('๐Ÿ”˜ Step 6: Button names...'));
4377
+ const buttonResults = await this.fixButtonNames(directory);
4378
+ const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
4379
+ const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4380
+ results.steps.push({ step: 6, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
4381
+
4382
+ // Step 7: Link names
4383
+ console.log(chalk.blue('๐Ÿ”— Step 7: Link names...'));
4384
+ const linkResults = await this.fixLinkNames(directory);
4385
+ const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
4386
+ const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4387
+ results.steps.push({ step: 7, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
4388
+
4389
+ // Step 8: Landmarks
4390
+ console.log(chalk.blue('๐Ÿ›๏ธ Step 8: Landmarks...'));
4391
+ const landmarkResults = await this.fixLandmarks(directory);
4392
+ const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
4393
+ const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4394
+ results.steps.push({ step: 8, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
4395
+
4396
+ // Step 9: Heading analysis
4397
+ console.log(chalk.blue('๐Ÿ“‘ Step 9: Heading analysis...'));
4398
+ const headingResults = await this.analyzeHeadings(directory);
4399
+ const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4400
+ results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
4401
+ console.log(chalk.gray('๐Ÿ’ก Heading issues require manual review and cannot be auto-fixed'));
4402
+
4403
+ // Step 10: Broken links check
4404
+ console.log(chalk.blue('๐Ÿ”— Step 10: Broken links check...'));
4405
+ const brokenLinksResults = await this.checkBrokenLinks(directory);
4406
+ const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
4407
+ results.steps.push({ step: 10, name: 'Broken links check', issues: totalBrokenLinks });
4408
+ console.log(chalk.gray('๐Ÿ’ก Broken link issues require manual review and cannot be auto-fixed'));
4409
+
4410
+ // Step 11: Cleanup duplicate roles
4411
+ console.log(chalk.blue('๐Ÿงน Step 11: Cleanup duplicate roles...'));
4412
+ const cleanupResults = await this.cleanupDuplicateRoles(directory);
4413
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
4414
+ results.steps.push({ step: 11, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
4415
+
4416
+ // Calculate totals
4417
+ results.totalFiles = Math.max(
4418
+ langResults.length, altResults.length, roleResults.length, formResults.length,
4419
+ nestedResults.length, buttonResults.length, linkResults.length, landmarkResults.length,
4420
+ headingResults.length, brokenLinksResults.length, cleanupResults.length
4421
+ );
4422
+
4423
+ results.fixedFiles = new Set([
4424
+ ...langResults.filter(r => r.status === 'fixed').map(r => r.file),
4425
+ ...altResults.filter(r => r.status === 'fixed').map(r => r.file),
4426
+ ...roleResults.filter(r => r.status === 'fixed').map(r => r.file),
4427
+ ...formResults.filter(r => r.status === 'fixed').map(r => r.file),
4428
+ ...nestedResults.filter(r => r.status === 'fixed').map(r => r.file),
4429
+ ...buttonResults.filter(r => r.status === 'fixed').map(r => r.file),
4430
+ ...linkResults.filter(r => r.status === 'fixed').map(r => r.file),
4431
+ ...landmarkResults.filter(r => r.status === 'fixed').map(r => r.file),
4432
+ ...cleanupResults.filter(r => r.status === 'fixed').map(r => r.file)
4433
+ ]).size;
4434
+
4435
+ results.totalIssues = totalAltIssues + totalRoleIssues + totalFormIssues + totalNestedIssues +
4436
+ totalButtonIssues + totalLinkIssues + totalLandmarkIssues;
4437
+
4438
+ // Final summary
4439
+ console.log(chalk.green('\n๐ŸŽ‰ All accessibility fixes completed!'));
4440
+ console.log(chalk.blue('๐Ÿ“Š Final Summary:'));
4441
+ console.log(chalk.blue(` Total files scanned: ${results.totalFiles}`));
4442
+ console.log(chalk.blue(` Files fixed: ${results.fixedFiles}`));
4443
+ console.log(chalk.blue(` Total issues resolved: ${results.totalIssues}`));
4444
+
4445
+ if (this.config.dryRun) {
4446
+ console.log(chalk.yellow('\n๐Ÿ’ก This was a dry run. Use without --dry-run to apply changes.'));
4447
+ }
4448
+
4449
+ return results;
4450
+
4451
+ } catch (error) {
4452
+ console.error(chalk.red(`โŒ Error during comprehensive fixes: ${error.message}`));
4453
+ throw error;
4454
+ }
4455
+ }
4456
+
3180
4457
  async findHtmlFiles(directory) {
3181
4458
  const files = [];
3182
4459