gbu-accessibility-package 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fixer.js ADDED
@@ -0,0 +1,764 @@
1
+ /**
2
+ * Accessibility Fixer
3
+ * Automated fixes for common accessibility issues
4
+ */
5
+
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+ const chalk = require('chalk');
9
+
10
+ class AccessibilityFixer {
11
+ constructor(config = {}) {
12
+ this.config = {
13
+ backupFiles: config.backupFiles !== false,
14
+ language: config.language || 'ja',
15
+ dryRun: config.dryRun || false,
16
+ ...config
17
+ };
18
+ }
19
+
20
+ async fixHtmlLang(directory = '.') {
21
+ console.log(chalk.blue('šŸ“ Fixing HTML lang attributes...'));
22
+
23
+ const htmlFiles = await this.findHtmlFiles(directory);
24
+ const results = [];
25
+
26
+ for (const file of htmlFiles) {
27
+ try {
28
+ const content = await fs.readFile(file, 'utf8');
29
+ const fixed = this.fixLangAttribute(content);
30
+
31
+ if (fixed !== content) {
32
+ if (this.config.backupFiles) {
33
+ await fs.writeFile(`${file}.backup`, content);
34
+ }
35
+
36
+ if (!this.config.dryRun) {
37
+ await fs.writeFile(file, fixed);
38
+ }
39
+
40
+ console.log(chalk.green(`āœ… Fixed lang attribute in: ${file}`));
41
+ results.push({ file, status: 'fixed' });
42
+ } else {
43
+ results.push({ file, status: 'no-change' });
44
+ }
45
+ } catch (error) {
46
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
47
+ results.push({ file, status: 'error', error: error.message });
48
+ }
49
+ }
50
+
51
+ return results;
52
+ }
53
+
54
+ async fixEmptyAltAttributes(directory = '.') {
55
+ console.log(chalk.blue('šŸ–¼ļø Fixing empty alt attributes...'));
56
+
57
+ const htmlFiles = await this.findHtmlFiles(directory);
58
+ const results = [];
59
+ let totalIssuesFound = 0;
60
+
61
+ for (const file of htmlFiles) {
62
+ try {
63
+ const content = await fs.readFile(file, 'utf8');
64
+ const issues = this.analyzeAltAttributes(content);
65
+
66
+ if (issues.length > 0) {
67
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
68
+ issues.forEach(issue => {
69
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
70
+ totalIssuesFound++;
71
+ });
72
+ }
73
+
74
+ const fixed = this.fixAltAttributes(content);
75
+
76
+ if (fixed !== content) {
77
+ if (this.config.backupFiles) {
78
+ await fs.writeFile(`${file}.backup`, content);
79
+ }
80
+
81
+ if (!this.config.dryRun) {
82
+ await fs.writeFile(file, fixed);
83
+ }
84
+
85
+ console.log(chalk.green(`āœ… Fixed alt attributes in: ${file}`));
86
+ results.push({ file, status: 'fixed', issues: issues.length });
87
+ } else {
88
+ results.push({ file, status: 'no-change', issues: issues.length });
89
+ }
90
+ } catch (error) {
91
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
92
+ results.push({ file, status: 'error', error: error.message });
93
+ }
94
+ }
95
+
96
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} alt attribute issues across ${results.length} files`));
97
+ return results;
98
+ }
99
+
100
+ analyzeAltAttributes(content) {
101
+ const issues = [];
102
+ const imgRegex = /<img[^>]*>/gi;
103
+ const imgTags = content.match(imgRegex) || [];
104
+
105
+ imgTags.forEach((imgTag, index) => {
106
+ const hasAlt = /alt\s*=/i.test(imgTag);
107
+ const hasEmptyAlt = /alt\s*=\s*[""''][""'']/i.test(imgTag);
108
+ const src = imgTag.match(/src\s*=\s*["']([^"']+)["']/i);
109
+ const srcValue = src ? src[1] : 'unknown';
110
+
111
+ if (!hasAlt) {
112
+ issues.push({
113
+ type: 'āŒ Missing alt',
114
+ description: `Image ${index + 1} (${srcValue}) has no alt attribute`,
115
+ imgTag: imgTag.substring(0, 100) + '...'
116
+ });
117
+ } else if (hasEmptyAlt) {
118
+ issues.push({
119
+ type: 'āš ļø Empty alt',
120
+ description: `Image ${index + 1} (${srcValue}) has empty alt attribute`,
121
+ imgTag: imgTag.substring(0, 100) + '...'
122
+ });
123
+ }
124
+ });
125
+
126
+ return issues;
127
+ }
128
+
129
+ async fixRoleAttributes(directory = '.') {
130
+ console.log(chalk.blue('šŸŽ­ Fixing role attributes...'));
131
+
132
+ const htmlFiles = await this.findHtmlFiles(directory);
133
+ const results = [];
134
+ let totalIssuesFound = 0;
135
+
136
+ for (const file of htmlFiles) {
137
+ try {
138
+ const content = await fs.readFile(file, 'utf8');
139
+ const issues = this.analyzeRoleAttributes(content);
140
+
141
+ if (issues.length > 0) {
142
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
143
+ issues.forEach(issue => {
144
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
145
+ totalIssuesFound++;
146
+ });
147
+ }
148
+
149
+ const fixed = this.fixRoleAttributesInContent(content);
150
+
151
+ if (fixed !== content) {
152
+ if (this.config.backupFiles) {
153
+ await fs.writeFile(`${file}.backup`, content);
154
+ }
155
+
156
+ if (!this.config.dryRun) {
157
+ await fs.writeFile(file, fixed);
158
+ }
159
+
160
+ console.log(chalk.green(`āœ… Fixed role attributes in: ${file}`));
161
+ results.push({ file, status: 'fixed', issues: issues.length });
162
+ } else {
163
+ results.push({ file, status: 'no-change', issues: issues.length });
164
+ }
165
+ } catch (error) {
166
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
167
+ results.push({ file, status: 'error', error: error.message });
168
+ }
169
+ }
170
+
171
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} role attribute issues across ${results.length} files`));
172
+ return results;
173
+ }
174
+
175
+ async addMainLandmarks(directory = '.') {
176
+ console.log(chalk.yellow('šŸ—ļø Main landmark detection (manual review required)...'));
177
+
178
+ const htmlFiles = await this.findHtmlFiles(directory);
179
+ const suggestions = [];
180
+
181
+ for (const file of htmlFiles) {
182
+ const content = await fs.readFile(file, 'utf8');
183
+
184
+ if (!content.includes('<main')) {
185
+ const mainCandidates = this.findMainContentCandidates(content);
186
+ suggestions.push({
187
+ file,
188
+ candidates: mainCandidates,
189
+ recommendation: 'Add <main> element around primary content'
190
+ });
191
+ }
192
+ }
193
+
194
+ return suggestions;
195
+ }
196
+
197
+ fixLangAttribute(content) {
198
+ const langValue = this.config.language;
199
+
200
+ return content
201
+ .replace(/<html class="no-js" lang="">/g, `<html class="no-js" lang="${langValue}">`)
202
+ .replace(/<html class="no-js">/g, `<html class="no-js" lang="${langValue}">`)
203
+ .replace(/<html lang="">/g, `<html lang="${langValue}">`)
204
+ .replace(/<html>/g, `<html lang="${langValue}">`);
205
+ }
206
+
207
+ fixAltAttributes(content) {
208
+ let fixed = content;
209
+ let changesMade = false;
210
+
211
+ // Find all img tags and process them
212
+ const imgRegex = /<img[^>]*>/gi;
213
+ const imgTags = content.match(imgRegex) || [];
214
+
215
+ for (let i = 0; i < imgTags.length; i++) {
216
+ const imgTag = imgTags[i];
217
+ let newImgTag = imgTag;
218
+
219
+ // Check if img has alt attribute
220
+ const hasAlt = /alt\s*=/i.test(imgTag);
221
+ const hasEmptyAlt = /alt\s*=\s*[""'']\s*[""'']/i.test(imgTag);
222
+
223
+ if (!hasAlt) {
224
+ // Add alt attribute if missing - use contextual analysis
225
+ const altText = this.generateAltText(imgTag, content, i);
226
+ newImgTag = imgTag.replace(/(<img[^>]*)(>)/i, `$1 alt="${altText}"$2`);
227
+ changesMade = true;
228
+ console.log(chalk.yellow(` āš ļø Added missing alt attribute: ${imgTag.substring(0, 50)}...`));
229
+ console.log(chalk.green(` → "${altText}"`));
230
+ } else if (hasEmptyAlt) {
231
+ // Fix empty alt attributes based on context
232
+ const altText = this.generateAltText(imgTag, content, i);
233
+ newImgTag = imgTag.replace(/alt\s*=\s*[""''][""'']/i, `alt="${altText}"`);
234
+ changesMade = true;
235
+ console.log(chalk.yellow(` āœļø Fixed empty alt attribute: ${imgTag.substring(0, 50)}...`));
236
+ console.log(chalk.green(` → "${altText}"`));
237
+ }
238
+
239
+ if (newImgTag !== imgTag) {
240
+ fixed = fixed.replace(imgTag, newImgTag);
241
+ }
242
+ }
243
+
244
+ return fixed;
245
+ }
246
+
247
+ generateAltText(imgTag, htmlContent = '', imgIndex = 0) {
248
+ const src = imgTag.match(/src\s*=\s*["']([^"']+)["']/i);
249
+ const srcValue = src ? src[1].toLowerCase() : '';
250
+
251
+ // Try to find contextual text around the image
252
+ const contextualText = this.findContextualText(imgTag, htmlContent, imgIndex);
253
+ if (contextualText) {
254
+ return contextualText;
255
+ }
256
+
257
+ // Generate appropriate alt text based on image source
258
+ if (srcValue.includes('logo')) {
259
+ return 'ćƒ­ć‚“';
260
+ } else if (srcValue.includes('icon')) {
261
+ return 'ć‚¢ć‚¤ć‚³ćƒ³';
262
+ } else if (srcValue.includes('banner')) {
263
+ return 'バナー';
264
+ } else if (srcValue.includes('button')) {
265
+ return 'ćƒœć‚æćƒ³';
266
+ } else if (srcValue.includes('arrow')) {
267
+ return 'ēŸ¢å°';
268
+ } else if (srcValue.includes('calendar')) {
269
+ return 'ć‚«ćƒ¬ćƒ³ćƒ€ćƒ¼';
270
+ } else if (srcValue.includes('video')) {
271
+ return 'ćƒ“ćƒ‡ć‚Ŗ';
272
+ } else if (srcValue.includes('chart') || srcValue.includes('graph')) {
273
+ return 'ć‚°ćƒ©ćƒ•';
274
+ } else if (srcValue.includes('photo') || srcValue.includes('img')) {
275
+ return 'å†™ēœŸ';
276
+ } else {
277
+ return 'ē”»åƒ';
278
+ }
279
+ }
280
+
281
+ findContextualText(imgTag, htmlContent, imgIndex) {
282
+ if (!htmlContent) return null;
283
+
284
+ // Find the position of this specific img tag in the content
285
+ const imgPosition = this.findImgPosition(imgTag, htmlContent, imgIndex);
286
+ if (imgPosition === -1) return null;
287
+
288
+ // Extract surrounding context (500 chars before and after)
289
+ const contextStart = Math.max(0, imgPosition - 500);
290
+ const contextEnd = Math.min(htmlContent.length, imgPosition + imgTag.length + 500);
291
+ const context = htmlContent.substring(contextStart, contextEnd);
292
+
293
+ // Try different strategies to find relevant text
294
+ const strategies = [
295
+ () => this.findTitleAttribute(imgTag),
296
+ () => this.findDtText(context, imgTag),
297
+ () => this.findParentLinkText(context, imgTag),
298
+ () => this.findNearbyHeadings(context, imgTag),
299
+ () => this.findFigcaptionText(context, imgTag),
300
+ () => this.findNearbyText(context, imgTag),
301
+ () => this.findAriaLabel(imgTag)
302
+ ];
303
+
304
+ for (const strategy of strategies) {
305
+ const result = strategy();
306
+ if (result && result.trim().length > 0 && result.trim().length <= 100) {
307
+ return this.cleanText(result);
308
+ }
309
+ }
310
+
311
+ return null;
312
+ }
313
+
314
+ findImgPosition(imgTag, htmlContent, imgIndex) {
315
+ const imgRegex = /<img[^>]*>/gi;
316
+ let match;
317
+ let currentIndex = 0;
318
+
319
+ while ((match = imgRegex.exec(htmlContent)) !== null) {
320
+ if (currentIndex === imgIndex) {
321
+ return match.index;
322
+ }
323
+ currentIndex++;
324
+ }
325
+
326
+ return -1;
327
+ }
328
+
329
+ findTitleAttribute(imgTag) {
330
+ const titleMatch = imgTag.match(/title\s*=\s*["']([^"']+)["']/i);
331
+ return titleMatch ? titleMatch[1] : null;
332
+ }
333
+
334
+ findAriaLabel(imgTag) {
335
+ const ariaMatch = imgTag.match(/aria-label\s*=\s*["']([^"']+)["']/i);
336
+ return ariaMatch ? ariaMatch[1] : null;
337
+ }
338
+
339
+ findDtText(context, imgTag) {
340
+ // Look for dt (definition term) elements near the image
341
+ const imgPos = context.indexOf(imgTag.substring(0, 50));
342
+ if (imgPos === -1) return null;
343
+
344
+ // Get surrounding context (larger range for dt detection)
345
+ const contextStart = Math.max(0, imgPos - 800);
346
+ const contextEnd = Math.min(context.length, imgPos + imgTag.length + 800);
347
+ const surroundingContext = context.substring(contextStart, contextEnd);
348
+
349
+ // Look for dt elements in various container patterns
350
+ const dtPatterns = [
351
+ // Pattern 1: dt inside dl near image
352
+ /<dl[^>]*>[\s\S]*?<dt[^>]*>([^<]+)<\/dt>[\s\S]*?<\/dl>/gi,
353
+ // Pattern 2: dt in definition list with dd containing image
354
+ /<dt[^>]*>([^<]+)<\/dt>[\s\S]*?<dd[^>]*>[\s\S]*?<img[^>]*>[\s\S]*?<\/dd>/gi,
355
+ // Pattern 3: dt followed by content containing image
356
+ /<dt[^>]*>([^<]+)<\/dt>[\s\S]{0,500}?<img[^>]*>/gi,
357
+ // Pattern 4: Simple dt near image
358
+ /<dt[^>]*>([^<]+)<\/dt>/gi
359
+ ];
360
+
361
+ for (const pattern of dtPatterns) {
362
+ const matches = [...surroundingContext.matchAll(pattern)];
363
+
364
+ for (const match of matches) {
365
+ // Check if this dt is related to our image
366
+ if (this.isRelatedToImage(match[0], imgTag, surroundingContext)) {
367
+ const dtText = match[1].trim();
368
+ if (dtText && dtText.length > 0 && dtText.length <= 100) {
369
+ console.log(chalk.blue(` šŸ“… Found dt text: "${dtText}"`));
370
+ return dtText;
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ return null;
377
+ }
378
+
379
+ isRelatedToImage(dtBlock, imgTag, context) {
380
+ // Check if the dt block contains or is near the image
381
+ const dtPos = context.indexOf(dtBlock);
382
+ const imgPos = context.indexOf(imgTag.substring(0, 50));
383
+
384
+ if (dtPos === -1 || imgPos === -1) return false;
385
+
386
+ // If image is within the dt block
387
+ if (dtBlock.includes(imgTag.substring(0, 50))) {
388
+ return true;
389
+ }
390
+
391
+ // If dt and image are close to each other (within 600 characters)
392
+ const distance = Math.abs(dtPos - imgPos);
393
+ return distance <= 600;
394
+ }
395
+
396
+ findParentLinkText(context, imgTag) {
397
+ // Find if image is inside a link and get link text
398
+ const linkPattern = /<a[^>]*>([^<]*<img[^>]*>[^<]*)<\/a>/gi;
399
+ const matches = context.match(linkPattern);
400
+
401
+ if (matches) {
402
+ for (const match of matches) {
403
+ if (match.includes(imgTag.substring(0, 50))) {
404
+ // Extract text content from the link
405
+ const textMatch = match.match(/>([^<]+)</g);
406
+ if (textMatch) {
407
+ const linkText = textMatch
408
+ .map(t => t.replace(/[><]/g, '').trim())
409
+ .filter(t => t.length > 0 && !t.includes('img'))
410
+ .join(' ');
411
+ if (linkText) return linkText;
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ return null;
418
+ }
419
+
420
+ findNearbyHeadings(context, imgTag) {
421
+ // Look for headings (h1-h6) near the image
422
+ const headingPattern = /<h[1-6][^>]*>([^<]+)<\/h[1-6]>/gi;
423
+ const headings = [];
424
+ let match;
425
+
426
+ while ((match = headingPattern.exec(context)) !== null) {
427
+ headings.push({
428
+ text: match[1].trim(),
429
+ position: match.index
430
+ });
431
+ }
432
+
433
+ if (headings.length > 0) {
434
+ // Find the closest heading
435
+ const imgPos = context.indexOf(imgTag.substring(0, 50));
436
+ let closest = headings[0];
437
+ let minDistance = Math.abs(closest.position - imgPos);
438
+
439
+ for (const heading of headings) {
440
+ const distance = Math.abs(heading.position - imgPos);
441
+ if (distance < minDistance) {
442
+ closest = heading;
443
+ minDistance = distance;
444
+ }
445
+ }
446
+
447
+ return closest.text;
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ findFigcaptionText(context, imgTag) {
454
+ // Look for figcaption associated with the image
455
+ const figurePattern = /<figure[^>]*>[\s\S]*?<figcaption[^>]*>([^<]+)<\/figcaption>[\s\S]*?<\/figure>/gi;
456
+ const matches = context.match(figurePattern);
457
+
458
+ if (matches) {
459
+ for (const match of matches) {
460
+ if (match.includes(imgTag.substring(0, 50))) {
461
+ const captionMatch = match.match(/<figcaption[^>]*>([^<]+)<\/figcaption>/i);
462
+ if (captionMatch) {
463
+ return captionMatch[1].trim();
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ return null;
470
+ }
471
+
472
+ findNearbyText(context, imgTag) {
473
+ // Look for text in nearby elements (p, div, span)
474
+ const imgPos = context.indexOf(imgTag.substring(0, 50));
475
+ if (imgPos === -1) return null;
476
+
477
+ // Get text before and after the image
478
+ const beforeText = context.substring(Math.max(0, imgPos - 200), imgPos);
479
+ const afterText = context.substring(imgPos + imgTag.length, imgPos + imgTag.length + 200);
480
+
481
+ // Extract meaningful text from nearby elements
482
+ const textPattern = /<(?:p|div|span|h[1-6])[^>]*>([^<]+)<\/(?:p|div|span|h[1-6])>/gi;
483
+ const texts = [];
484
+
485
+ let match;
486
+ while ((match = textPattern.exec(beforeText + afterText)) !== null) {
487
+ const text = match[1].trim();
488
+ if (text.length > 5 && text.length <= 50) {
489
+ texts.push(text);
490
+ }
491
+ }
492
+
493
+ // Return the most relevant text (shortest meaningful one)
494
+ if (texts.length > 0) {
495
+ return texts.sort((a, b) => a.length - b.length)[0];
496
+ }
497
+
498
+ return null;
499
+ }
500
+
501
+ cleanText(text) {
502
+ return text
503
+ .replace(/\s+/g, ' ')
504
+ .replace(/[^\w\s\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g, '')
505
+ .trim()
506
+ .substring(0, 100);
507
+ }
508
+
509
+ analyzeRoleAttributes(content) {
510
+ const issues = [];
511
+
512
+ // Define elements that should have specific roles
513
+ const roleRules = [
514
+ {
515
+ selector: /<button[^>]*>/gi,
516
+ expectedRole: 'button',
517
+ description: 'Button element should have role="button" or be implicit'
518
+ },
519
+ {
520
+ selector: /<a[^>]*href[^>]*>/gi,
521
+ expectedRole: 'link',
522
+ description: 'Link element should have role="link" or be implicit'
523
+ },
524
+ {
525
+ selector: /<img[^>]*>/gi,
526
+ expectedRole: 'img',
527
+ description: 'Image element should have role="img" or be implicit'
528
+ },
529
+ {
530
+ selector: /<ul[^>]*>/gi,
531
+ expectedRole: 'list',
532
+ description: 'Unordered list should have role="list"'
533
+ },
534
+ {
535
+ selector: /<ol[^>]*>/gi,
536
+ expectedRole: 'list',
537
+ description: 'Ordered list should have role="list"'
538
+ },
539
+ {
540
+ selector: /<li[^>]*>/gi,
541
+ expectedRole: 'listitem',
542
+ description: 'List item should have role="listitem"'
543
+ },
544
+ {
545
+ selector: /<nav[^>]*>/gi,
546
+ expectedRole: 'navigation',
547
+ description: 'Navigation element should have role="navigation"'
548
+ },
549
+ {
550
+ selector: /<main[^>]*>/gi,
551
+ expectedRole: 'main',
552
+ description: 'Main element should have role="main"'
553
+ },
554
+ {
555
+ selector: /<header[^>]*>/gi,
556
+ expectedRole: 'banner',
557
+ description: 'Header element should have role="banner"'
558
+ },
559
+ {
560
+ selector: /<footer[^>]*>/gi,
561
+ expectedRole: 'contentinfo',
562
+ description: 'Footer element should have role="contentinfo"'
563
+ }
564
+ ];
565
+
566
+ // Check for images that need role="img"
567
+ const images = content.match(/<img[^>]*>/gi) || [];
568
+ images.forEach((img, index) => {
569
+ if (!img.includes('role=')) {
570
+ issues.push({
571
+ type: 'šŸ–¼ļø Missing role',
572
+ description: `Image ${index + 1} should have role="img"`,
573
+ element: img.substring(0, 100) + '...'
574
+ });
575
+ }
576
+ });
577
+
578
+ // Check for button elements with onclick that need role
579
+ const buttonsWithOnclick = content.match(/<button[^>]*onclick[^>]*>/gi) || [];
580
+ buttonsWithOnclick.forEach((button, index) => {
581
+ if (!button.includes('role=')) {
582
+ issues.push({
583
+ type: 'šŸ”˜ Missing role',
584
+ description: `Button ${index + 1} with onclick should have role="button"`,
585
+ element: button.substring(0, 100) + '...'
586
+ });
587
+ }
588
+ });
589
+
590
+ // Check for anchor elements that need role
591
+ const anchors = content.match(/<a[^>]*href[^>]*>/gi) || [];
592
+ anchors.forEach((anchor, index) => {
593
+ if (!anchor.includes('role=')) {
594
+ issues.push({
595
+ type: 'šŸ”— Missing role',
596
+ description: `Anchor ${index + 1} should have role="link"`,
597
+ element: anchor.substring(0, 100) + '...'
598
+ });
599
+ }
600
+ });
601
+
602
+ // Check for any element with onclick that needs role
603
+ const elementsWithOnclick = content.match(/<(?!a|button)[a-zA-Z][a-zA-Z0-9]*[^>]*onclick[^>]*>/gi) || [];
604
+ elementsWithOnclick.forEach((element, index) => {
605
+ if (!element.includes('role=')) {
606
+ const tagMatch = element.match(/<([a-zA-Z][a-zA-Z0-9]*)/);
607
+ const tagName = tagMatch ? tagMatch[1] : 'element';
608
+ issues.push({
609
+ type: 'šŸ”˜ Missing role',
610
+ description: `${tagName} ${index + 1} with onclick should have role="button"`,
611
+ element: element.substring(0, 100) + '...'
612
+ });
613
+ }
614
+ });
615
+
616
+ // Check for clickable divs that should have button role
617
+ const clickableDivs = content.match(/<div[^>]*(?:onclick|class="[^"]*(?:btn|button|click)[^"]*")[^>]*>/gi) || [];
618
+ clickableDivs.forEach((div, index) => {
619
+ if (!div.includes('role=')) {
620
+ issues.push({
621
+ type: 'šŸ”˜ Missing role',
622
+ description: `Clickable div ${index + 1} should have role="button"`,
623
+ element: div.substring(0, 100) + '...'
624
+ });
625
+ }
626
+ });
627
+
628
+ // Check for elements with tabindex that might need roles
629
+ const tabindexElements = content.match(/<(?!a|button|input|select|textarea)[^>]*tabindex\s*=\s*[""']?[0-9-]+[""']?[^>]*>/gi) || [];
630
+ tabindexElements.forEach((element, index) => {
631
+ if (!element.includes('role=')) {
632
+ issues.push({
633
+ type: 'āŒØļø Missing role',
634
+ description: `Focusable element ${index + 1} should have appropriate role`,
635
+ element: element.substring(0, 100) + '...'
636
+ });
637
+ }
638
+ });
639
+
640
+ return issues;
641
+ }
642
+
643
+ fixRoleAttributesInContent(content) {
644
+ let fixed = content;
645
+
646
+ // Fix all images - add role="img" (only if no role exists)
647
+ fixed = fixed.replace(
648
+ /<img([^>]*)(?!.*role\s*=)([^>]*>)/gi,
649
+ (match, attrs, end) => {
650
+ console.log(chalk.yellow(` šŸ–¼ļø Added role="img" to image element`));
651
+ return `<img${attrs} role="img"${end}`;
652
+ }
653
+ );
654
+
655
+ // Fix button elements with onclick - add role="button"
656
+ fixed = fixed.replace(
657
+ /<button([^>]*onclick[^>]*)(?!.*role\s*=)([^>]*>)/gi,
658
+ (match, attrs, end) => {
659
+ console.log(chalk.yellow(` šŸ”˜ Added role="button" to button with onclick`));
660
+ return `<button${attrs} role="button"${end}`;
661
+ }
662
+ );
663
+
664
+ // Fix anchor elements - add role="link"
665
+ fixed = fixed.replace(
666
+ /<a([^>]*href[^>]*)(?!.*role\s*=)([^>]*>)/gi,
667
+ (match, attrs, end) => {
668
+ console.log(chalk.yellow(` šŸ”— Added role="link" to anchor element`));
669
+ return `<a${attrs} role="link"${end}`;
670
+ }
671
+ );
672
+
673
+ // Fix any element with onclick (except a and button) - add role="button"
674
+ fixed = fixed.replace(
675
+ /<((?!a|button)[a-zA-Z][a-zA-Z0-9]*)([^>]*onclick[^>]*)(?!.*role\s*=)([^>]*>)/gi,
676
+ (match, tag, attrs, end) => {
677
+ console.log(chalk.yellow(` šŸ”˜ Added role="button" to ${tag} with onclick`));
678
+ return `<${tag}${attrs} role="button"${end}`;
679
+ }
680
+ );
681
+
682
+ // Fix clickable divs - add role="button"
683
+ fixed = fixed.replace(
684
+ /<div([^>]*class="[^"]*(?:btn|button|click)[^"]*"[^>]*)(?!.*role\s*=)([^>]*>)/gi,
685
+ (match, attrs, end) => {
686
+ console.log(chalk.yellow(` šŸ”˜ Added role="button" to clickable div`));
687
+ return `<div${attrs} role="button"${end}`;
688
+ }
689
+ );
690
+
691
+ // Fix focusable elements with tabindex
692
+ fixed = fixed.replace(
693
+ /<(div|span)([^>]*tabindex\s*=\s*[""']?[0-9-]+[""']?[^>]*)(?!.*role\s*=)([^>]*>)/gi,
694
+ (match, tag, attrs, end) => {
695
+ console.log(chalk.yellow(` āŒØļø Added role="button" to focusable ${tag}`));
696
+ return `<${tag}${attrs} role="button"${end}`;
697
+ }
698
+ );
699
+
700
+ // Fix navigation lists that should be menus
701
+ fixed = fixed.replace(
702
+ /<ul([^>]*class="[^"]*(?:nav|menu)[^"]*"[^>]*)(?!.*role\s*=)([^>]*>)/gi,
703
+ (match, attrs, end) => {
704
+ console.log(chalk.yellow(` šŸ“‹ Added role="menubar" to navigation list`));
705
+ return `<ul${attrs} role="menubar"${end}`;
706
+ }
707
+ );
708
+
709
+ // Fix list items in navigation menus
710
+ fixed = fixed.replace(
711
+ /<li([^>]*class="[^"]*(?:nav|menu)[^"]*"[^>]*)(?!.*role\s*=)([^>]*>)/gi,
712
+ (match, attrs, end) => {
713
+ console.log(chalk.yellow(` šŸ“‹ Added role="menuitem" to navigation list item`));
714
+ return `<li${attrs} role="menuitem"${end}`;
715
+ }
716
+ );
717
+
718
+ return fixed;
719
+ }
720
+
721
+ findMainContentCandidates(content) {
722
+ const candidates = [];
723
+
724
+ // Look for common main content patterns
725
+ const patterns = [
726
+ /<div[^>]*class="[^"]*main[^"]*"/gi,
727
+ /<div[^>]*class="[^"]*content[^"]*"/gi,
728
+ /<section[^>]*class="[^"]*main[^"]*"/gi,
729
+ /<article/gi
730
+ ];
731
+
732
+ patterns.forEach(pattern => {
733
+ const matches = content.match(pattern);
734
+ if (matches) {
735
+ candidates.push(...matches);
736
+ }
737
+ });
738
+
739
+ return candidates;
740
+ }
741
+
742
+ async findHtmlFiles(directory) {
743
+ const files = [];
744
+
745
+ async function scan(dir) {
746
+ const entries = await fs.readdir(dir, { withFileTypes: true });
747
+
748
+ for (const entry of entries) {
749
+ const fullPath = path.join(dir, entry.name);
750
+
751
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
752
+ await scan(fullPath);
753
+ } else if (entry.isFile() && entry.name.endsWith('.html')) {
754
+ files.push(fullPath);
755
+ }
756
+ }
757
+ }
758
+
759
+ await scan(directory);
760
+ return files;
761
+ }
762
+ }
763
+
764
+ module.exports = AccessibilityFixer;