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 +19 -0
- package/README-vi.md +12 -3
- package/README.md +12 -3
- package/cli.js +55 -10
- package/demo/heading-structure-test.html +60 -0
- package/lib/fixer.js +634 -0
- package/package.json +1 -1
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
|
|
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
|
|
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à
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
414
|
-
console.log(chalk.blue('📑 Running heading
|
|
415
|
-
const headingResults = await fixer.
|
|
416
|
-
const
|
|
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
|
-
|
|
419
|
-
|
|
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, '
|
|
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.
|
|
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": {
|