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/QUICK_START.md +38 -0
- package/README-vi.md +85 -0
- package/README.md +82 -0
- package/cli.js +23 -1
- package/demo/form-labels-test.html +87 -0
- package/demo/nested-controls-test.html +92 -0
- package/lib/fixer.js +1277 -0
- package/package.json +1 -1
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
|
|