gbu-accessibility-package 3.4.0 → 3.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.5.0] - 2025-01-08
9
+
10
+ ### Added
11
+ - **Enhanced Heading Structure Auto-Fix**: Automatic fixing of heading hierarchy issues with `--auto-fix-headings` option
12
+ - **Improved Nested Controls Detection**: Better detection and fixing of nested interactive controls
13
+ - **Advanced Test Cases**: New comprehensive test files for heading structure and nested controls
14
+ - **Enhanced Alt Text Quality**: Improved context-aware alt text generation with better vocabulary support
15
+
16
+ ### Enhanced
17
+ - **Heading Analysis**: More comprehensive detection of heading issues including empty headings, level skipping, and duplicates
18
+ - **Interactive Controls**: Better handling of complex nested control scenarios
19
+ - **Test Coverage**: Expanded demo files for better testing and validation
20
+ - **Performance**: Optimized processing for large files and complex HTML structures
21
+
22
+ ### Fixed
23
+ - **Heading Level Corrections**: Automatic correction of improper heading hierarchies
24
+ - **Empty Heading Detection**: Better identification and handling of empty headings
25
+ - **Control Nesting Issues**: Improved resolution of nested interactive control conflicts
26
+
8
27
  ## [3.2.0] - 2024-07-28
9
28
 
10
29
  ### Added
package/README-vi.md CHANGED
@@ -18,7 +18,8 @@
18
18
  - 🔘 **Button Names** - Sửa buttons rỗng và input buttons không có tên
19
19
  - 🔗 **Link Names** - Sửa links rỗng và phát hiện text generic
20
20
  - 🏛️ **Landmarks** - Thêm main và navigation landmarks thiếu
21
- - 📑 **Phân tích Heading** - Phân tích cấu trúc heading với gợi ý (không tự động sửa)
21
+ - 📑 **Phân tích và Sửa Heading** - Phân tích cấu trúc heading với tùy chọn tự động sửa `--auto-fix-headings`
22
+ - 🎯 **Nested Controls Detection** - Phát hiện và sửa các control tương tác lồng nhau
22
23
  - 🔍 **Broken Links Detection** - Phát hiện liên kết bị hỏng và tài nguyên 404
23
24
  - 🧹 **Dọn dẹp Duplicate** - Loại bỏ role attributes trùng lặp
24
25
 
@@ -148,7 +149,8 @@ Chế độ sửa lỗi:
148
149
  --buttons-only Sửa button names + dọn dẹp
149
150
  --links-only Sửa link names + dọn dẹp
150
151
  --landmarks-only Sửa landmarks + dọn dẹp
151
- --headings-only Phân tích cấu trúc heading (không tự động sửa)
152
+ --headings-only Phân tích cấu trúc heading với tùy chọn tự động sửa
153
+ --auto-fix-headings Bật tự động sửa lỗi heading structure
152
154
  --links-check Kiểm tra liên kết bị hỏng và tài nguyên 404
153
155
  --cleanup-only Chỉ dọn dẹp role attributes trùng lặp
154
156
 
@@ -178,6 +180,8 @@ gbu-a11y -l en ./public
178
180
  gbu-a11y --alt-only # Sửa alt attributes + dọn dẹp
179
181
  gbu-a11y --forms-only # Sửa form labels + dọn dẹp
180
182
  gbu-a11y --buttons-only # Sửa button names + dọn dẹp
183
+ gbu-a11y --headings-only # Phân tích heading structure
184
+ gbu-a11y --headings-only --auto-fix-headings # Tự động sửa heading structure
181
185
  gbu-a11y --links-check # Kiểm tra liên kết bị hỏng + dọn dẹp
182
186
 
183
187
  # Tính năng enhanced alt attribute
@@ -308,7 +312,12 @@ console.log("Hoàn thành sửa lỗi với enhanced features:", results);
308
312
 
309
313
  - **Lang attributes thiếu** → Phát hiện ngôn ngữ tự động
310
314
  - **Landmark thiếu** → Main và navigation landmarks
311
- - **Cấu trúc heading** → Phân tích và khuyến nghị
315
+ - **Cấu trúc heading** → Phân tích và tự động sửa với `--auto-fix-headings`
316
+ - Sửa multiple h1 elements
317
+ - Sửa heading level skipping (h2 → h4)
318
+ - Thêm text cho empty headings
319
+ - Sửa duplicate headings
320
+ - **Nested interactive controls** → Phát hiện và sửa controls lồng nhau
312
321
  - **Role attributes** → Gán role tuân thủ WCAG
313
322
 
314
323
  ### Kiểm tra liên kết
package/README.md CHANGED
@@ -17,7 +17,8 @@
17
17
  - 🔘 **Button Names** - Fix empty buttons and input buttons without names
18
18
  - 🔗 **Link Names** - Fix empty links and detect generic link text
19
19
  - 🏛️ **Landmarks** - Add missing main and navigation landmarks
20
- - 📑 **Heading Analysis** - Analyze heading structure with suggestions (no auto-fix)
20
+ - 📑 **Heading Analysis & Auto-Fix** - Analyze heading structure with optional auto-fix using `--auto-fix-headings`
21
+ - 🎯 **Nested Controls Detection** - Detect and fix nested interactive controls
21
22
  - 🔍 **Broken Links Detection** - Detect broken links and 404 resources
22
23
  - 🧹 **Duplicate Cleanup** - Remove duplicate role attributes
23
24
 
@@ -145,7 +146,8 @@ Fix Modes:
145
146
  --buttons-only Fix button names + cleanup
146
147
  --links-only Fix link names + cleanup
147
148
  --landmarks-only Fix landmarks + cleanup
148
- --headings-only Analyze heading structure (no auto-fix)
149
+ --headings-only Analyze heading structure with optional auto-fix
150
+ --auto-fix-headings Enable automatic heading structure fixes
149
151
  --links-check Check for broken links and 404 resources
150
152
  --cleanup-only Only cleanup duplicate role attributes
151
153
 
@@ -175,6 +177,8 @@ gbu-a11y -l en ./public
175
177
  gbu-a11y --alt-only # Fix alt attributes + cleanup
176
178
  gbu-a11y --forms-only # Fix form labels + cleanup
177
179
  gbu-a11y --buttons-only # Fix button names + cleanup
180
+ gbu-a11y --headings-only # Analyze heading structure
181
+ gbu-a11y --headings-only --auto-fix-headings # Auto-fix heading structure
178
182
  gbu-a11y --links-check # Check broken links + cleanup
179
183
 
180
184
  # Enhanced alt attribute features
@@ -298,7 +302,12 @@ console.log('Accessibility fixes completed with enhanced features:', results);
298
302
  ### Document Structure
299
303
  - **Missing lang attributes** → Automatic language detection
300
304
  - **Missing landmarks** → Main and navigation landmarks
301
- - **Heading structure** → Analysis and recommendations
305
+ - **Heading structure** → Analysis and auto-fix with `--auto-fix-headings`
306
+ - Fix multiple h1 elements
307
+ - Fix heading level skipping (h2 → h4)
308
+ - Add text to empty headings
309
+ - Fix duplicate headings
310
+ - **Nested interactive controls** → Detect and fix nested controls
302
311
  - **Role attributes** → WCAG-compliant role assignments
303
312
 
304
313
  ### Link Validation
package/cli.js CHANGED
@@ -28,12 +28,16 @@ const options = {
28
28
  linksOnly: false,
29
29
  landmarksOnly: false,
30
30
  headingsOnly: false,
31
+ dlOnly: false,
31
32
  brokenLinksOnly: false,
32
33
  // Enhanced alt options
33
34
  enhancedAlt: false,
34
35
  altCreativity: 'balanced', // conservative, balanced, creative
35
36
  includeEmotions: false,
36
- strictAltChecking: false
37
+ strictAltChecking: false,
38
+ // Advanced features options
39
+ autoFixHeadings: false,
40
+ fixDescriptionLists: true
37
41
  };
38
42
 
39
43
  // Parse arguments
@@ -96,10 +100,19 @@ for (let i = 0; i < args.length; i++) {
96
100
  case '--headings-only':
97
101
  options.headingsOnly = true;
98
102
  break;
103
+ case '--dl-only':
104
+ options.dlOnly = true;
105
+ break;
99
106
  case '--links-check':
100
107
  case '--broken-links':
101
108
  options.brokenLinksOnly = true;
102
109
  break;
110
+ case '--auto-fix-headings':
111
+ options.autoFixHeadings = true;
112
+ break;
113
+ case '--no-fix-dl':
114
+ options.fixDescriptionLists = false;
115
+ break;
103
116
  case '--enhanced-alt':
104
117
  options.enhancedAlt = true;
105
118
  break;
@@ -225,14 +238,16 @@ async function main() {
225
238
  enhancedAltMode: options.enhancedAlt,
226
239
  altCreativity: options.altCreativity,
227
240
  includeEmotions: options.includeEmotions,
228
- strictAltChecking: options.strictAltChecking
241
+ strictAltChecking: options.strictAltChecking,
242
+ autoFixHeadings: options.autoFixHeadings,
243
+ fixDescriptionLists: options.fixDescriptionLists
229
244
  });
230
245
 
231
246
  try {
232
247
  // Handle different modes - All modes now include cleanup
233
248
  if (options.cleanupOnly || options.altOnly || options.langOnly || options.roleOnly ||
234
249
  options.formsOnly || options.nestedOnly || options.buttonsOnly || options.linksOnly || options.landmarksOnly ||
235
- options.headingsOnly || options.brokenLinksOnly) {
250
+ options.headingsOnly || options.dlOnly || options.brokenLinksOnly) {
236
251
  // Individual modes - handle each separately, then run cleanup
237
252
  } else {
238
253
  // Default mode: Run comprehensive fix (all fixes including cleanup)
@@ -410,15 +425,45 @@ async function main() {
410
425
  return;
411
426
 
412
427
  } else if (options.headingsOnly) {
413
- // Analyze headings only (no fixes, no cleanup)
414
- console.log(chalk.blue('📑 Running heading analysis only...'));
415
- const headingResults = await fixer.analyzeHeadings(options.directory);
416
- const totalSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
428
+ // Fix heading structure + cleanup
429
+ console.log(chalk.blue('📑 Running heading structure fixes + cleanup...'));
430
+ const headingResults = await fixer.fixHeadingStructure(options.directory);
431
+ const headingFixed = headingResults.filter(r => r.status === 'fixed').length;
432
+ const totalHeadingIssues = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
433
+ const totalHeadingFixes = headingResults.reduce((sum, r) => sum + (r.fixes || 0), 0);
434
+
435
+ console.log(chalk.green(`\n✅ Processed headings in ${headingResults.length} files (${totalHeadingIssues} issues found)`));
436
+ if (options.autoFixHeadings) {
437
+ console.log(chalk.green(`✅ Fixed ${totalHeadingFixes} heading issues automatically`));
438
+ } else {
439
+ console.log(chalk.gray('💡 Use --auto-fix-headings to enable automatic heading fixes'));
440
+ }
441
+
442
+ // Run cleanup
443
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
444
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
445
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
446
+ console.log(chalk.green(`✅ Cleaned duplicate roles in ${cleanupFixed} files`));
417
447
 
418
- console.log(chalk.green(`\n✅ Analyzed headings in ${headingResults.length} files (${totalSuggestions} suggestions)`));
419
- console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
448
+ showCompletionMessage(options, 'Heading structure fixes + cleanup');
449
+ return;
450
+
451
+ } else if (options.dlOnly) {
452
+ // Fix description lists + cleanup
453
+ console.log(chalk.blue('📋 Running description list fixes + cleanup...'));
454
+ const dlResults = await fixer.fixDescriptionLists(options.directory);
455
+ const dlFixed = dlResults.filter(r => r.status === 'fixed').length;
456
+ const totalDlIssues = dlResults.reduce((sum, r) => sum + (r.issues || 0), 0);
457
+
458
+ console.log(chalk.green(`\n✅ Fixed description lists in ${dlFixed} files (${totalDlIssues} issues)`));
459
+
460
+ // Run cleanup
461
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
462
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
463
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
464
+ console.log(chalk.green(`✅ Cleaned duplicate roles in ${cleanupFixed} files`));
420
465
 
421
- showCompletionMessage(options, 'Heading analysis');
466
+ showCompletionMessage(options, 'Description list fixes + cleanup');
422
467
  return;
423
468
 
424
469
  } else if (options.brokenLinksOnly) {
@@ -0,0 +1,60 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Heading Structure Test - Accessibility Issues</title>
7
+ </head>
8
+ <body>
9
+ <h1>Heading Structure Test Cases</h1>
10
+
11
+ <!-- Test Case 1: Missing h1 (this will be converted from h2) -->
12
+ <h2>This should be h1</h2>
13
+ <p>Main content of the page</p>
14
+
15
+ <!-- Test Case 2: Multiple h1 elements -->
16
+ <h1>First h1 (should stay)</h1>
17
+ <h1>Second h1 (should become h2)</h1>
18
+ <h1>Third h1 (should become h2)</h1>
19
+
20
+ <!-- Test Case 3: Level skipping -->
21
+ <h2>Section heading</h2>
22
+ <h4>Subsection (skips h3 - should become h3)</h4>
23
+ <h6>Sub-subsection (skips h4, h5 - should become h4)</h6>
24
+
25
+ <!-- Test Case 4: Empty headings -->
26
+ <h3></h3>
27
+ <h4> </h4>
28
+ <h5><span></span></h5>
29
+
30
+ <!-- Test Case 5: Duplicate headings -->
31
+ <h2>Products</h2>
32
+ <p>Some content about products</p>
33
+ <h2>Products</h2>
34
+ <p>More content about products</p>
35
+
36
+ <!-- Test Case 6: Proper structure (should not be changed) -->
37
+ <h2>Proper Section</h2>
38
+ <h3>Proper Subsection</h3>
39
+ <h4>Proper Sub-subsection</h4>
40
+ <p>Content with proper heading hierarchy</p>
41
+
42
+ <!-- Test Case 7: Complex nesting with issues -->
43
+ <h2>Services</h2>
44
+ <h5>Service 1 (should be h3)</h5>
45
+ <h6>Details (should be h4)</h6>
46
+ <h3>Service 2 (correct)</h3>
47
+ <h5>More details (should be h4)</h5>
48
+
49
+ <!-- Test Case 8: Empty headings with context -->
50
+ <div class="section">
51
+ <h3></h3>
52
+ <p>This section talks about our company history and achievements.</p>
53
+ </div>
54
+
55
+ <div class="product-info">
56
+ <h4> </h4>
57
+ <p>Our flagship product offers innovative solutions for modern businesses.</p>
58
+ </div>
59
+ </body>
60
+ </html>
package/lib/fixer.js CHANGED
@@ -1236,6 +1236,9 @@ class AccessibilityFixer {
1236
1236
  altCreativity: config.altCreativity || 'balanced', // conservative, balanced, creative
1237
1237
  includeEmotions: config.includeEmotions || false,
1238
1238
  strictAltChecking: config.strictAltChecking || false,
1239
+ // New options for advanced features
1240
+ autoFixHeadings: config.autoFixHeadings || false, // Enable automatic heading fixes
1241
+ fixDescriptionLists: config.fixDescriptionLists || true, // Enable DL structure fixes
1239
1242
  ...config
1240
1243
  };
1241
1244
 
@@ -4454,6 +4457,637 @@ class AccessibilityFixer {
4454
4457
  }
4455
4458
  }
4456
4459
 
4460
+ async fixHeadingStructure(directory = '.') {
4461
+ console.log(chalk.blue('📑 Fixing heading structure...'));
4462
+
4463
+ const htmlFiles = await this.findHtmlFiles(directory);
4464
+ const results = [];
4465
+ let totalIssuesFound = 0;
4466
+ let totalIssuesFixed = 0;
4467
+
4468
+ for (const file of htmlFiles) {
4469
+ try {
4470
+ const content = await fs.readFile(file, 'utf8');
4471
+ const analysis = this.analyzeHeadingStructure(content);
4472
+
4473
+ if (analysis.issues.length > 0) {
4474
+ console.log(chalk.cyan(`\n📁 ${file}:`));
4475
+ analysis.issues.forEach(issue => {
4476
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4477
+ if (issue.suggestion) {
4478
+ console.log(chalk.gray(` 💡 ${issue.suggestion}`));
4479
+ }
4480
+ totalIssuesFound++;
4481
+ });
4482
+ }
4483
+
4484
+ let fixed = content;
4485
+ let changesMade = false;
4486
+
4487
+ if (this.config.autoFixHeadings) {
4488
+ fixed = this.fixHeadingStructureInContent(content, analysis);
4489
+ changesMade = fixed !== content;
4490
+
4491
+ if (changesMade) {
4492
+ const fixedCount = this.countHeadingFixes(content, fixed);
4493
+ totalIssuesFixed += fixedCount;
4494
+
4495
+ if (this.config.backupFiles) {
4496
+ await fs.writeFile(`${file}.backup`, content);
4497
+ }
4498
+
4499
+ if (!this.config.dryRun) {
4500
+ await fs.writeFile(file, fixed);
4501
+ }
4502
+
4503
+ console.log(chalk.green(`✅ Fixed heading structure in: ${file} (${fixedCount} fixes)`));
4504
+ results.push({ file, status: 'fixed', issues: analysis.issues.length, fixes: fixedCount });
4505
+ } else {
4506
+ results.push({ file, status: 'no-change', issues: analysis.issues.length });
4507
+ }
4508
+ } else {
4509
+ results.push({ file, status: 'analyzed', issues: analysis.issues.length });
4510
+ }
4511
+ } catch (error) {
4512
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
4513
+ results.push({ file, status: 'error', error: error.message });
4514
+ }
4515
+ }
4516
+
4517
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} heading issues across ${results.length} files`));
4518
+ if (this.config.autoFixHeadings) {
4519
+ console.log(chalk.green(` Fixed ${totalIssuesFixed} heading issues automatically`));
4520
+ } else {
4521
+ console.log(chalk.gray('💡 Use --auto-fix-headings option to enable automatic fixes'));
4522
+ }
4523
+
4524
+ return results;
4525
+ }
4526
+
4527
+ analyzeHeadingStructure(content) {
4528
+ const issues = [];
4529
+ const suggestions = [];
4530
+
4531
+ // Extract all headings with their levels, text, and positions
4532
+ const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
4533
+ const headings = [];
4534
+ let match;
4535
+
4536
+ while ((match = headingPattern.exec(content)) !== null) {
4537
+ const level = parseInt(match[1]);
4538
+ const rawText = match[2];
4539
+ const text = rawText.replace(/<[^>]*>/g, '').trim();
4540
+ const fullTag = match[0];
4541
+
4542
+ headings.push({
4543
+ level,
4544
+ text,
4545
+ rawText,
4546
+ fullTag,
4547
+ position: match.index,
4548
+ originalMatch: match[0]
4549
+ });
4550
+ }
4551
+
4552
+ if (headings.length === 0) {
4553
+ issues.push({
4554
+ type: '📑 No headings found',
4555
+ description: 'Page has no heading elements',
4556
+ suggestion: 'Add heading elements (h1-h6) to structure content',
4557
+ severity: 'error',
4558
+ fixable: false
4559
+ });
4560
+ return { issues, suggestions, headings };
4561
+ }
4562
+
4563
+ // Check for h1
4564
+ const h1Count = headings.filter(h => h.level === 1).length;
4565
+ if (h1Count === 0) {
4566
+ issues.push({
4567
+ type: '📑 Missing h1',
4568
+ description: 'Page should have exactly one h1 element',
4569
+ suggestion: 'Add an h1 element as the main page heading',
4570
+ severity: 'error',
4571
+ fixable: true,
4572
+ fix: 'add-h1'
4573
+ });
4574
+ } else if (h1Count > 1) {
4575
+ issues.push({
4576
+ type: '📑 Multiple h1 elements',
4577
+ description: `Found ${h1Count} h1 elements, should have only one`,
4578
+ suggestion: 'Convert extra h1 elements to h2-h6 as appropriate',
4579
+ severity: 'error',
4580
+ fixable: true,
4581
+ fix: 'fix-multiple-h1'
4582
+ });
4583
+ }
4584
+
4585
+ // Check heading order and hierarchy
4586
+ for (let i = 1; i < headings.length; i++) {
4587
+ const current = headings[i];
4588
+ const previous = headings[i - 1];
4589
+
4590
+ // Check for level skipping
4591
+ if (current.level > previous.level + 1) {
4592
+ issues.push({
4593
+ type: '📑 Heading level skip',
4594
+ description: `Heading level jumps from h${previous.level} to h${current.level}`,
4595
+ suggestion: `Use h${previous.level + 1} instead of h${current.level}`,
4596
+ severity: 'warning',
4597
+ fixable: true,
4598
+ fix: 'fix-level-skip',
4599
+ targetIndex: i,
4600
+ correctLevel: previous.level + 1
4601
+ });
4602
+ }
4603
+ }
4604
+
4605
+ // Check for empty headings
4606
+ headings.forEach((heading, index) => {
4607
+ if (!heading.text) {
4608
+ issues.push({
4609
+ type: '📑 Empty heading',
4610
+ description: `Heading ${index + 1} (h${heading.level}) is empty`,
4611
+ suggestion: 'Add descriptive text to the heading or remove it',
4612
+ severity: 'error',
4613
+ fixable: true,
4614
+ fix: 'fix-empty-heading',
4615
+ targetIndex: index
4616
+ });
4617
+ }
4618
+ });
4619
+
4620
+ // Check for consecutive headings with same level and similar content
4621
+ for (let i = 1; i < headings.length; i++) {
4622
+ const current = headings[i];
4623
+ const previous = headings[i - 1];
4624
+
4625
+ if (current.level === previous.level &&
4626
+ current.text.toLowerCase() === previous.text.toLowerCase() &&
4627
+ current.text.length > 0) {
4628
+ issues.push({
4629
+ type: '📑 Duplicate heading',
4630
+ description: `Duplicate h${current.level} heading: "${current.text}"`,
4631
+ suggestion: 'Make heading text unique or merge content',
4632
+ severity: 'warning',
4633
+ fixable: false
4634
+ });
4635
+ }
4636
+ }
4637
+
4638
+ return { issues, suggestions, headings };
4639
+ }
4640
+
4641
+ fixHeadingStructureInContent(content, analysis) {
4642
+ let fixed = content;
4643
+ const { issues, headings } = analysis;
4644
+
4645
+ // Sort issues by position (descending) to avoid position shifts
4646
+ const fixableIssues = issues
4647
+ .filter(issue => issue.fixable)
4648
+ .sort((a, b) => (b.targetIndex || 0) - (a.targetIndex || 0));
4649
+
4650
+ fixableIssues.forEach(issue => {
4651
+ switch (issue.fix) {
4652
+ case 'add-h1':
4653
+ fixed = this.addMissingH1(fixed);
4654
+ break;
4655
+
4656
+ case 'fix-multiple-h1':
4657
+ fixed = this.fixMultipleH1(fixed, headings);
4658
+ break;
4659
+
4660
+ case 'fix-level-skip':
4661
+ if (issue.targetIndex !== undefined && issue.correctLevel) {
4662
+ fixed = this.fixHeadingLevelSkip(fixed, headings[issue.targetIndex], issue.correctLevel);
4663
+ }
4664
+ break;
4665
+
4666
+ case 'fix-empty-heading':
4667
+ if (issue.targetIndex !== undefined) {
4668
+ fixed = this.fixEmptyHeading(fixed, headings[issue.targetIndex]);
4669
+ }
4670
+ break;
4671
+ }
4672
+ });
4673
+
4674
+ return fixed;
4675
+ }
4676
+
4677
+ addMissingH1(content) {
4678
+ // Try to find the first heading and convert it to h1
4679
+ const firstHeadingMatch = content.match(/<h([2-6])[^>]*>([\s\S]*?)<\/h[2-6]>/i);
4680
+
4681
+ if (firstHeadingMatch) {
4682
+ const level = firstHeadingMatch[1];
4683
+ const replacement = firstHeadingMatch[0].replace(
4684
+ new RegExp(`<h${level}([^>]*)>`, 'i'),
4685
+ '<h1$1>'
4686
+ ).replace(
4687
+ new RegExp(`</h${level}>`, 'i'),
4688
+ '</h1>'
4689
+ );
4690
+
4691
+ const result = content.replace(firstHeadingMatch[0], replacement);
4692
+ console.log(chalk.yellow(` 📑 Converted first h${level} to h1`));
4693
+ return result;
4694
+ }
4695
+
4696
+ // If no headings found, try to add h1 based on title or first significant text
4697
+ const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
4698
+ if (titleMatch) {
4699
+ const title = titleMatch[1].trim();
4700
+ // Insert h1 after opening body tag
4701
+ const bodyMatch = content.match(/(<body[^>]*>)/i);
4702
+ if (bodyMatch) {
4703
+ const h1Element = `\n <h1>${title}</h1>\n`;
4704
+ const result = content.replace(bodyMatch[1], bodyMatch[1] + h1Element);
4705
+ console.log(chalk.yellow(` 📑 Added h1 element with title: "${title}"`));
4706
+ return result;
4707
+ }
4708
+ }
4709
+
4710
+ return content;
4711
+ }
4712
+
4713
+ fixMultipleH1(content, headings) {
4714
+ const h1Elements = headings.filter(h => h.level === 1);
4715
+
4716
+ // Keep the first h1, convert others to h2
4717
+ for (let i = 1; i < h1Elements.length; i++) {
4718
+ const h1 = h1Elements[i];
4719
+ const replacement = h1.fullTag.replace(/<h1([^>]*)>/i, '<h2$1>').replace(/<\/h1>/i, '</h2>');
4720
+ content = content.replace(h1.fullTag, replacement);
4721
+ console.log(chalk.yellow(` 📑 Converted extra h1 to h2: "${h1.text}"`));
4722
+ }
4723
+
4724
+ return content;
4725
+ }
4726
+
4727
+ fixHeadingLevelSkip(content, heading, correctLevel) {
4728
+ const replacement = heading.fullTag
4729
+ .replace(new RegExp(`<h${heading.level}([^>]*)>`, 'i'), `<h${correctLevel}$1>`)
4730
+ .replace(new RegExp(`</h${heading.level}>`, 'i'), `</h${correctLevel}>`);
4731
+
4732
+ const result = content.replace(heading.fullTag, replacement);
4733
+ console.log(chalk.yellow(` 📑 Fixed level skip: h${heading.level} → h${correctLevel} for "${heading.text}"`));
4734
+ return result;
4735
+ }
4736
+
4737
+ fixEmptyHeading(content, heading) {
4738
+ // Generate meaningful text based on context
4739
+ const contextText = this.generateHeadingText(content, heading);
4740
+
4741
+ if (contextText) {
4742
+ const replacement = heading.fullTag.replace(
4743
+ /<h([1-6])([^>]*)>[\s\S]*?<\/h[1-6]>/i,
4744
+ `<h$1$2>${contextText}</h$1>`
4745
+ );
4746
+
4747
+ const result = content.replace(heading.fullTag, replacement);
4748
+ console.log(chalk.yellow(` 📑 Added text to empty heading: "${contextText}"`));
4749
+ return result;
4750
+ }
4751
+
4752
+ // If can't generate text, remove the empty heading
4753
+ const result = content.replace(heading.fullTag, '');
4754
+ console.log(chalk.yellow(` 📑 Removed empty h${heading.level} heading`));
4755
+ return result;
4756
+ }
4757
+
4758
+ generateHeadingText(content, heading) {
4759
+ const lang = this.config.language;
4760
+
4761
+ // Try to find nearby text content
4762
+ const position = heading.position;
4763
+ const contextRange = 500;
4764
+ const beforeContext = content.substring(Math.max(0, position - contextRange), position);
4765
+ const afterContext = content.substring(position + heading.fullTag.length, position + heading.fullTag.length + contextRange);
4766
+
4767
+ // Look for meaningful text in nearby paragraphs
4768
+ const nearbyText = (beforeContext + afterContext).replace(/<[^>]*>/g, ' ').trim();
4769
+ const words = nearbyText.split(/\s+/).filter(word => word.length > 2);
4770
+
4771
+ if (words.length > 0) {
4772
+ const meaningfulWords = words.slice(0, 3);
4773
+ return meaningfulWords.join(' ');
4774
+ }
4775
+
4776
+ // Fallback to generic text based on language
4777
+ const genericTexts = {
4778
+ ja: ['見出し', 'セクション', 'コンテンツ'],
4779
+ en: ['Heading', 'Section', 'Content'],
4780
+ vi: ['Tiêu đề', 'Phần', 'Nội dung']
4781
+ };
4782
+
4783
+ const texts = genericTexts[lang] || genericTexts.en;
4784
+ return texts[0];
4785
+ }
4786
+
4787
+ countHeadingFixes(originalContent, fixedContent) {
4788
+ // Count the number of heading-related changes
4789
+ const originalHeadings = (originalContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
4790
+ const fixedHeadings = (fixedContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
4791
+
4792
+ // Simple heuristic: count tag changes
4793
+ let changes = 0;
4794
+
4795
+ // Count h1 additions
4796
+ const originalH1 = (originalContent.match(/<h1[^>]*>/gi) || []).length;
4797
+ const fixedH1 = (fixedContent.match(/<h1[^>]*>/gi) || []).length;
4798
+ changes += Math.abs(fixedH1 - originalH1);
4799
+
4800
+ // Count level changes (rough estimate)
4801
+ for (let level = 1; level <= 6; level++) {
4802
+ const originalCount = (originalContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
4803
+ const fixedCount = (fixedContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
4804
+ changes += Math.abs(fixedCount - originalCount);
4805
+ }
4806
+
4807
+ return Math.max(1, Math.floor(changes / 2)); // Rough estimate
4808
+ }
4809
+
4810
+ async fixDescriptionLists(directory = '.') {
4811
+ console.log(chalk.blue('📋 Fixing description list structure...'));
4812
+
4813
+ const htmlFiles = await this.findHtmlFiles(directory);
4814
+ const results = [];
4815
+ let totalIssuesFound = 0;
4816
+
4817
+ for (const file of htmlFiles) {
4818
+ try {
4819
+ const content = await fs.readFile(file, 'utf8');
4820
+ const issues = this.analyzeDescriptionListStructure(content);
4821
+
4822
+ if (issues.length > 0) {
4823
+ console.log(chalk.cyan(`\n📁 ${file}:`));
4824
+ issues.forEach(issue => {
4825
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4826
+ if (issue.suggestion) {
4827
+ console.log(chalk.gray(` 💡 ${issue.suggestion}`));
4828
+ }
4829
+ totalIssuesFound++;
4830
+ });
4831
+ }
4832
+
4833
+ const fixed = this.fixDescriptionListStructureInContent(content);
4834
+
4835
+ if (fixed !== content) {
4836
+ if (this.config.backupFiles) {
4837
+ await fs.writeFile(`${file}.backup`, content);
4838
+ }
4839
+
4840
+ if (!this.config.dryRun) {
4841
+ await fs.writeFile(file, fixed);
4842
+ }
4843
+
4844
+ console.log(chalk.green(`✅ Fixed description list structure in: ${file}`));
4845
+ results.push({ file, status: 'fixed', issues: issues.length });
4846
+ } else {
4847
+ results.push({ file, status: 'no-change', issues: issues.length });
4848
+ }
4849
+ } catch (error) {
4850
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
4851
+ results.push({ file, status: 'error', error: error.message });
4852
+ }
4853
+ }
4854
+
4855
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} description list issues across ${results.length} files`));
4856
+ return results;
4857
+ }
4858
+
4859
+ analyzeDescriptionListStructure(content) {
4860
+ const issues = [];
4861
+
4862
+ // Find all dl elements
4863
+ const dlPattern = /<dl[^>]*>([\s\S]*?)<\/dl>/gi;
4864
+ let dlMatch;
4865
+ let dlIndex = 0;
4866
+
4867
+ while ((dlMatch = dlPattern.exec(content)) !== null) {
4868
+ dlIndex++;
4869
+ const dlContent = dlMatch[1];
4870
+ const dlElement = dlMatch[0];
4871
+
4872
+ // Analyze the content inside dl
4873
+ const dtElements = (dlContent.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || []);
4874
+ const ddElements = (dlContent.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || []);
4875
+
4876
+ // Check for empty dl
4877
+ if (dtElements.length === 0 && ddElements.length === 0) {
4878
+ issues.push({
4879
+ type: '📋 Empty description list',
4880
+ description: `Description list ${dlIndex} is empty`,
4881
+ suggestion: 'Add dt/dd pairs or remove the empty dl element',
4882
+ severity: 'error',
4883
+ dlIndex,
4884
+ fix: 'remove-empty-dl'
4885
+ });
4886
+ continue;
4887
+ }
4888
+
4889
+ // Check for missing dt elements
4890
+ if (dtElements.length === 0 && ddElements.length > 0) {
4891
+ issues.push({
4892
+ type: '📋 Missing dt elements',
4893
+ description: `Description list ${dlIndex} has dd elements but no dt elements`,
4894
+ suggestion: 'Add dt elements to describe the dd content',
4895
+ severity: 'error',
4896
+ dlIndex,
4897
+ fix: 'add-missing-dt'
4898
+ });
4899
+ }
4900
+
4901
+ // Check for missing dd elements
4902
+ if (dtElements.length > 0 && ddElements.length === 0) {
4903
+ issues.push({
4904
+ type: '📋 Missing dd elements',
4905
+ description: `Description list ${dlIndex} has dt elements but no dd elements`,
4906
+ suggestion: 'Add dd elements to provide descriptions',
4907
+ severity: 'error',
4908
+ dlIndex,
4909
+ fix: 'add-missing-dd'
4910
+ });
4911
+ }
4912
+
4913
+ // Check for improper nesting (non-dt/dd elements directly in dl)
4914
+ const invalidChildren = dlContent.match(/<(?!dt|dd|\/dt|\/dd)[a-zA-Z][^>]*>/g);
4915
+ if (invalidChildren) {
4916
+ const invalidTags = [...new Set(invalidChildren.map(tag => tag.match(/<([a-zA-Z]+)/)[1]))];
4917
+ issues.push({
4918
+ type: '📋 Invalid dl children',
4919
+ description: `Description list ${dlIndex} contains invalid child elements: ${invalidTags.join(', ')}`,
4920
+ suggestion: 'Only dt and dd elements should be direct children of dl',
4921
+ severity: 'warning',
4922
+ dlIndex,
4923
+ fix: 'wrap-invalid-children'
4924
+ });
4925
+ }
4926
+
4927
+ // Check for empty dt/dd elements
4928
+ dtElements.forEach((dt, index) => {
4929
+ const dtText = dt.replace(/<[^>]*>/g, '').trim();
4930
+ if (!dtText) {
4931
+ issues.push({
4932
+ type: '📋 Empty dt element',
4933
+ description: `Empty dt element in description list ${dlIndex}`,
4934
+ suggestion: 'Add descriptive text to the dt element',
4935
+ severity: 'warning',
4936
+ dlIndex,
4937
+ dtIndex: index,
4938
+ fix: 'fix-empty-dt'
4939
+ });
4940
+ }
4941
+ });
4942
+
4943
+ ddElements.forEach((dd, index) => {
4944
+ const ddText = dd.replace(/<[^>]*>/g, '').trim();
4945
+ if (!ddText) {
4946
+ issues.push({
4947
+ type: '📋 Empty dd element',
4948
+ description: `Empty dd element in description list ${dlIndex}`,
4949
+ suggestion: 'Add descriptive content to the dd element',
4950
+ severity: 'warning',
4951
+ dlIndex,
4952
+ ddIndex: index,
4953
+ fix: 'fix-empty-dd'
4954
+ });
4955
+ }
4956
+ });
4957
+
4958
+ // Check for proper dt/dd pairing
4959
+ if (dtElements.length > 0 && ddElements.length > 0) {
4960
+ // Basic check: should have at least one dd for each dt
4961
+ if (ddElements.length < dtElements.length) {
4962
+ issues.push({
4963
+ type: '📋 Insufficient dd elements',
4964
+ description: `Description list ${dlIndex} has ${dtElements.length} dt elements but only ${ddElements.length} dd elements`,
4965
+ suggestion: 'Each dt should have at least one corresponding dd element',
4966
+ severity: 'warning',
4967
+ dlIndex
4968
+ });
4969
+ }
4970
+ }
4971
+ }
4972
+
4973
+ return issues;
4974
+ }
4975
+
4976
+ fixDescriptionListStructureInContent(content) {
4977
+ let fixed = content;
4978
+
4979
+ // Fix empty dl elements
4980
+ fixed = fixed.replace(/<dl[^>]*>\s*<\/dl>/gi, (match) => {
4981
+ console.log(chalk.yellow(` 📋 Removed empty description list`));
4982
+ return '';
4983
+ });
4984
+
4985
+ // Fix dl elements with only whitespace
4986
+ fixed = fixed.replace(/<dl[^>]*>[\s\n\r]*<\/dl>/gi, (match) => {
4987
+ console.log(chalk.yellow(` 📋 Removed empty description list`));
4988
+ return '';
4989
+ });
4990
+
4991
+ // Fix dl elements with invalid direct children
4992
+ fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
4993
+ // Extract dt and dd elements
4994
+ const dtElements = content.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || [];
4995
+ const ddElements = content.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || [];
4996
+
4997
+ // Find invalid children (not dt or dd)
4998
+ let cleanContent = content;
4999
+
5000
+ // Remove invalid direct children by wrapping them in dd
5001
+ cleanContent = cleanContent.replace(/<(?!dt|dd|\/dt|\/dd)([a-zA-Z][^>]*)>([\s\S]*?)<\/[a-zA-Z]+>/gi, (invalidMatch, tag, innerContent) => {
5002
+ console.log(chalk.yellow(` 📋 Wrapped invalid child element in dd`));
5003
+ return `<dd>${invalidMatch}</dd>`;
5004
+ });
5005
+
5006
+ // Handle text nodes that are not in dt/dd
5007
+ cleanContent = cleanContent.replace(/^([^<]+)(?=<(?:dt|dd))/gm, (textMatch) => {
5008
+ const trimmed = textMatch.trim();
5009
+ if (trimmed) {
5010
+ console.log(chalk.yellow(` 📋 Wrapped loose text in dd`));
5011
+ return `<dd>${trimmed}</dd>`;
5012
+ }
5013
+ return '';
5014
+ });
5015
+
5016
+ return `<dl${attributes}>${cleanContent}</dl>`;
5017
+ });
5018
+
5019
+ // Add missing dd elements for dt elements without corresponding dd
5020
+ fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
5021
+ const dtPattern = /<dt[^>]*>([\s\S]*?)<\/dt>/gi;
5022
+ const ddPattern = /<dd[^>]*>[\s\S]*?<\/dd>/gi;
5023
+
5024
+ const dtMatches = [...content.matchAll(dtPattern)];
5025
+ const ddMatches = [...content.matchAll(ddPattern)];
5026
+
5027
+ if (dtMatches.length > 0 && ddMatches.length === 0) {
5028
+ // Add dd elements after each dt
5029
+ let fixedContent = content;
5030
+
5031
+ // Process from end to beginning to maintain positions
5032
+ for (let i = dtMatches.length - 1; i >= 0; i--) {
5033
+ const dtMatch = dtMatches[i];
5034
+ const dtText = dtMatch[1].replace(/<[^>]*>/g, '').trim();
5035
+ const ddText = this.generateDescriptionForTerm(dtText);
5036
+
5037
+ const insertPosition = dtMatch.index + dtMatch[0].length;
5038
+ fixedContent = fixedContent.slice(0, insertPosition) +
5039
+ `\n <dd>${ddText}</dd>` +
5040
+ fixedContent.slice(insertPosition);
5041
+ }
5042
+
5043
+ console.log(chalk.yellow(` 📋 Added missing dd elements for ${dtMatches.length} dt elements`));
5044
+ return `<dl${attributes}>${fixedContent}</dl>`;
5045
+ }
5046
+
5047
+ return match;
5048
+ });
5049
+
5050
+ // Fix empty dt/dd elements
5051
+ fixed = fixed.replace(/<dt[^>]*>\s*<\/dt>/gi, (match) => {
5052
+ const lang = this.config.language;
5053
+ const defaultText = lang === 'ja' ? '項目' : lang === 'vi' ? 'Mục' : 'Term';
5054
+ console.log(chalk.yellow(` 📋 Added text to empty dt element`));
5055
+ return match.replace(/>\s*</, `>${defaultText}<`);
5056
+ });
5057
+
5058
+ fixed = fixed.replace(/<dd[^>]*>\s*<\/dd>/gi, (match) => {
5059
+ const lang = this.config.language;
5060
+ const defaultText = lang === 'ja' ? '説明' : lang === 'vi' ? 'Mô tả' : 'Description';
5061
+ console.log(chalk.yellow(` 📋 Added text to empty dd element`));
5062
+ return match.replace(/>\s*</, `>${defaultText}<`);
5063
+ });
5064
+
5065
+ return fixed;
5066
+ }
5067
+
5068
+ generateDescriptionForTerm(termText) {
5069
+ const lang = this.config.language;
5070
+
5071
+ // Try to generate meaningful description based on term
5072
+ if (termText) {
5073
+ const descriptions = {
5074
+ ja: `${termText}の説明`,
5075
+ en: `Description of ${termText}`,
5076
+ vi: `Mô tả về ${termText}`
5077
+ };
5078
+ return descriptions[lang] || descriptions.en;
5079
+ }
5080
+
5081
+ // Fallback to generic description
5082
+ const fallbacks = {
5083
+ ja: '説明',
5084
+ en: 'Description',
5085
+ vi: 'Mô tả'
5086
+ };
5087
+
5088
+ return fallbacks[lang] || fallbacks.en;
5089
+ }
5090
+
4457
5091
  async findHtmlFiles(directory) {
4458
5092
  const files = [];
4459
5093
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbu-accessibility-package",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Comprehensive accessibility fixes for HTML files. Smart context-aware alt text generation, form labels, button names, link names, landmarks, heading analysis, and WCAG-compliant role attributes. Covers major axe DevTools issues with individual fix modes.",
5
5
  "main": "index.js",
6
6
  "bin": {