gbu-accessibility-package 1.0.0 → 1.1.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/README.md +33 -3
- package/cli.js +44 -2
- package/demo/duplicate-roles.html +45 -0
- package/demo/duplicate-roles.html.backup +45 -0
- package/lib/fixer.js +144 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,11 @@ Một công cụ tự động sửa các vấn đề accessibility phổ biến
|
|
|
14
14
|
- `role="link"` cho thẻ `<a>`
|
|
15
15
|
- `role="button"` cho elements có onclick
|
|
16
16
|
- `role="menubar"` và `role="menuitem"` cho navigation lists
|
|
17
|
+
- ✅ **Duplicate Cleanup**: Tự động xóa duplicate role attributes
|
|
17
18
|
- ✅ **Context-aware**: Phân tích nội dung xung quanh để tạo alt text phù hợp
|
|
18
19
|
- ✅ **Backup tự động**: Tạo backup files trước khi sửa
|
|
19
20
|
- ✅ **Dry run mode**: Xem preview trước khi apply changes
|
|
21
|
+
- ✅ **Comprehensive mode**: Chạy tất cả fixes trong một lần
|
|
20
22
|
|
|
21
23
|
## 📦 Cài đặt
|
|
22
24
|
|
|
@@ -61,8 +63,9 @@ npx gbu-a11y [options] [directory]
|
|
|
61
63
|
# Hoặc thêm vào package.json scripts
|
|
62
64
|
{
|
|
63
65
|
"scripts": {
|
|
64
|
-
"fix-a11y": "gbu-a11y",
|
|
65
|
-
"preview-a11y": "gbu-a11y --dry-run"
|
|
66
|
+
"fix-a11y": "gbu-a11y --comprehensive",
|
|
67
|
+
"preview-a11y": "gbu-a11y --comprehensive --dry-run",
|
|
68
|
+
"cleanup-roles": "gbu-a11y --cleanup-only"
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
```
|
|
@@ -163,7 +166,21 @@ Thêm role attributes cho các elements
|
|
|
163
166
|
const results = await fixer.fixRoleAttributes('./src');
|
|
164
167
|
```
|
|
165
168
|
|
|
166
|
-
### 4.
|
|
169
|
+
### 4. cleanupDuplicateRoles(directory)
|
|
170
|
+
Xóa duplicate role attributes
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
const results = await fixer.cleanupDuplicateRoles('./src');
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 5. fixAllAccessibilityIssues(directory)
|
|
177
|
+
Chạy tất cả fixes bao gồm cleanup (khuyến nghị)
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
const results = await fixer.fixAllAccessibilityIssues('./src');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 6. addMainLandmarks(directory)
|
|
167
184
|
Phát hiện và suggest main landmarks
|
|
168
185
|
|
|
169
186
|
```javascript
|
|
@@ -196,6 +213,19 @@ const suggestions = await fixer.addMainLandmarks('./src');
|
|
|
196
213
|
<button onclick="submit()" role="button">Submit</button>
|
|
197
214
|
```
|
|
198
215
|
|
|
216
|
+
### Duplicate Cleanup
|
|
217
|
+
```html
|
|
218
|
+
<!-- Trước -->
|
|
219
|
+
<img src="logo.png" alt="Logo" role="img" role="img" role="img">
|
|
220
|
+
<a href="/home" role="link" role="link">Home</a>
|
|
221
|
+
<button onclick="click()" role="button" role="button">Click</button>
|
|
222
|
+
|
|
223
|
+
<!-- Sau -->
|
|
224
|
+
<img src="logo.png" alt="Logo" role="img">
|
|
225
|
+
<a href="/home" role="link">Home</a>
|
|
226
|
+
<button onclick="click()" role="button">Click</button>
|
|
227
|
+
```
|
|
228
|
+
|
|
199
229
|
### Lang Attributes
|
|
200
230
|
```html
|
|
201
231
|
<!-- Trước -->
|
package/cli.js
CHANGED
|
@@ -16,7 +16,9 @@ const options = {
|
|
|
16
16
|
language: 'ja',
|
|
17
17
|
backupFiles: true,
|
|
18
18
|
dryRun: false,
|
|
19
|
-
help: false
|
|
19
|
+
help: false,
|
|
20
|
+
cleanupOnly: false,
|
|
21
|
+
comprehensive: false
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
// Parse arguments
|
|
@@ -42,6 +44,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
42
44
|
case '--dry-run':
|
|
43
45
|
options.dryRun = true;
|
|
44
46
|
break;
|
|
47
|
+
case '--cleanup-only':
|
|
48
|
+
options.cleanupOnly = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--comprehensive':
|
|
51
|
+
case '--all':
|
|
52
|
+
options.comprehensive = true;
|
|
53
|
+
break;
|
|
45
54
|
default:
|
|
46
55
|
if (!arg.startsWith('-')) {
|
|
47
56
|
options.directory = arg;
|
|
@@ -61,10 +70,14 @@ Options:
|
|
|
61
70
|
-l, --language <lang> Language for lang attribute (default: ja)
|
|
62
71
|
--no-backup Don't create backup files
|
|
63
72
|
--dry-run Preview changes without applying
|
|
73
|
+
--cleanup-only Only cleanup duplicate role attributes
|
|
74
|
+
--comprehensive, --all Run all fixes including cleanup (recommended)
|
|
64
75
|
-h, --help Show this help message
|
|
65
76
|
|
|
66
77
|
Examples:
|
|
67
|
-
node cli.js # Fix current directory
|
|
78
|
+
node cli.js # Fix current directory (standard fixes)
|
|
79
|
+
node cli.js --comprehensive # Run all fixes including cleanup
|
|
80
|
+
node cli.js --cleanup-only # Only cleanup duplicate roles
|
|
68
81
|
node cli.js ./src # Fix src directory
|
|
69
82
|
node cli.js -l en --dry-run ./dist # Preview fixes for dist directory in English
|
|
70
83
|
node cli.js --no-backup ./public # Fix without creating backups
|
|
@@ -95,6 +108,32 @@ async function main() {
|
|
|
95
108
|
});
|
|
96
109
|
|
|
97
110
|
try {
|
|
111
|
+
// Handle different modes
|
|
112
|
+
if (options.comprehensive) {
|
|
113
|
+
// Run comprehensive fix (all fixes including cleanup)
|
|
114
|
+
console.log(chalk.blue('🎯 Running comprehensive accessibility fixes...'));
|
|
115
|
+
const results = await fixer.fixAllAccessibilityIssues(options.directory);
|
|
116
|
+
|
|
117
|
+
// Results already logged in the method
|
|
118
|
+
return;
|
|
119
|
+
|
|
120
|
+
} else if (options.cleanupOnly) {
|
|
121
|
+
// Only cleanup duplicate roles
|
|
122
|
+
console.log(chalk.blue('🧹 Running cleanup for duplicate role attributes...'));
|
|
123
|
+
const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
|
|
124
|
+
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
125
|
+
|
|
126
|
+
console.log(chalk.green(`\n✅ Cleaned duplicate roles in ${cleanupFixed} files`));
|
|
127
|
+
|
|
128
|
+
if (options.dryRun) {
|
|
129
|
+
console.log(chalk.cyan('\n💡 This was a dry run. Use without --dry-run to apply changes.'));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.green('\n🎉 Cleanup completed successfully!'));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Standard mode - run individual fixes
|
|
98
137
|
// Fix HTML lang attributes
|
|
99
138
|
console.log(chalk.yellow('📝 Step 1: Fixing HTML lang attributes...'));
|
|
100
139
|
const langResults = await fixer.fixHtmlLang(options.directory);
|
|
@@ -146,6 +185,9 @@ async function main() {
|
|
|
146
185
|
console.log(chalk.gray(' Backup files created with .backup extension'));
|
|
147
186
|
}
|
|
148
187
|
}
|
|
188
|
+
|
|
189
|
+
// Suggest cleanup if not comprehensive mode
|
|
190
|
+
console.log(chalk.blue('\n💡 Pro tip: Use --comprehensive to include duplicate role cleanup!'));
|
|
149
191
|
|
|
150
192
|
} catch (error) {
|
|
151
193
|
console.error(chalk.red('❌ Error occurred:'), error.message);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Test Duplicate Roles</title>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<h1>Test Duplicate Role Attributes</h1>
|
|
8
|
+
|
|
9
|
+
<!-- Images with duplicate roles -->
|
|
10
|
+
<img src="logo.png" alt="Logo" role="img" role="img">
|
|
11
|
+
<img src="banner.jpg" alt="Banner" role="img" role="img" role="img">
|
|
12
|
+
|
|
13
|
+
<!-- Links with duplicate roles -->
|
|
14
|
+
<a href="/home" role="link" role="link">Home</a>
|
|
15
|
+
<a href="/about" role="link" role="link" role="link">About</a>
|
|
16
|
+
|
|
17
|
+
<!-- Buttons with duplicate roles -->
|
|
18
|
+
<button onclick="submit()" role="button" role="button">Submit</button>
|
|
19
|
+
<button type="button" role="button" role="button" role="button">Click Me</button>
|
|
20
|
+
|
|
21
|
+
<!-- Mixed quotes -->
|
|
22
|
+
<div onclick="toggle()" role="button" role='button'>Toggle</div>
|
|
23
|
+
<span onclick="show()" role='button' role="button">Show</span>
|
|
24
|
+
|
|
25
|
+
<!-- Navigation with duplicates -->
|
|
26
|
+
<nav>
|
|
27
|
+
<ul class="nav-menu" role="menubar" role="menubar">
|
|
28
|
+
<li class="nav-item" role="menuitem" role="menuitem">
|
|
29
|
+
<a href="/products" role="link" role="link">Products</a>
|
|
30
|
+
</li>
|
|
31
|
+
<li class="nav-item" role="menuitem" role="menuitem" role="menuitem">
|
|
32
|
+
<a href="/services" role="link" role="link" role="link">Services</a>
|
|
33
|
+
</li>
|
|
34
|
+
</ul>
|
|
35
|
+
</nav>
|
|
36
|
+
|
|
37
|
+
<!-- Complex duplicates -->
|
|
38
|
+
<article>
|
|
39
|
+
<h2>Article with Issues</h2>
|
|
40
|
+
<img src="article1.jpg" alt="Article Image" role="img" role="img">
|
|
41
|
+
<p>Some content...</p>
|
|
42
|
+
<a href="/read-more" role="link" role="link" role="link" role="link">Read More</a>
|
|
43
|
+
</article>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Test Duplicate Roles</title>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<h1>Test Duplicate Role Attributes</h1>
|
|
8
|
+
|
|
9
|
+
<!-- Images with duplicate roles -->
|
|
10
|
+
<img src="logo.png" alt="Logo" role="img" role="img">
|
|
11
|
+
<img src="banner.jpg" alt="Banner" role="img" role="img" role="img">
|
|
12
|
+
|
|
13
|
+
<!-- Links with duplicate roles -->
|
|
14
|
+
<a href="/home" role="link" role="link">Home</a>
|
|
15
|
+
<a href="/about" role="link" role="link" role="link">About</a>
|
|
16
|
+
|
|
17
|
+
<!-- Buttons with duplicate roles -->
|
|
18
|
+
<button onclick="submit()" role="button" role="button">Submit</button>
|
|
19
|
+
<button type="button" role="button" role="button" role="button">Click Me</button>
|
|
20
|
+
|
|
21
|
+
<!-- Mixed quotes -->
|
|
22
|
+
<div onclick="toggle()" role="button" role='button'>Toggle</div>
|
|
23
|
+
<span onclick="show()" role='button' role="button">Show</span>
|
|
24
|
+
|
|
25
|
+
<!-- Navigation with duplicates -->
|
|
26
|
+
<nav>
|
|
27
|
+
<ul class="nav-menu" role="menubar" role="menubar">
|
|
28
|
+
<li class="nav-item" role="menuitem" role="menuitem">
|
|
29
|
+
<a href="/products" role="link" role="link">Products</a>
|
|
30
|
+
</li>
|
|
31
|
+
<li class="nav-item" role="menuitem" role="menuitem" role="menuitem">
|
|
32
|
+
<a href="/services" role="link" role="link" role="link">Services</a>
|
|
33
|
+
</li>
|
|
34
|
+
</ul>
|
|
35
|
+
</nav>
|
|
36
|
+
|
|
37
|
+
<!-- Complex duplicates -->
|
|
38
|
+
<article>
|
|
39
|
+
<h2>Article with Issues</h2>
|
|
40
|
+
<img src="article1.jpg" alt="Article Image" role="img" role="img">
|
|
41
|
+
<p>Some content...</p>
|
|
42
|
+
<a href="/read-more" role="link" role="link" role="link" role="link">Read More</a>
|
|
43
|
+
</article>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
package/lib/fixer.js
CHANGED
|
@@ -739,9 +739,153 @@ class AccessibilityFixer {
|
|
|
739
739
|
return candidates;
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
+
async cleanupDuplicateRoles(directory = '.') {
|
|
743
|
+
console.log(chalk.blue('🧹 Cleaning up duplicate role attributes...'));
|
|
744
|
+
|
|
745
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
746
|
+
const results = [];
|
|
747
|
+
let totalFixedFiles = 0;
|
|
748
|
+
|
|
749
|
+
for (const file of htmlFiles) {
|
|
750
|
+
try {
|
|
751
|
+
const content = await fs.readFile(file, 'utf8');
|
|
752
|
+
const fixed = this.cleanupDuplicateRolesInContent(content);
|
|
753
|
+
|
|
754
|
+
if (fixed !== content) {
|
|
755
|
+
if (this.config.backupFiles) {
|
|
756
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (!this.config.dryRun) {
|
|
760
|
+
await fs.writeFile(file, fixed);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
console.log(chalk.green(`✅ Cleaned duplicate roles in: ${file}`));
|
|
764
|
+
results.push({ file, status: 'fixed' });
|
|
765
|
+
totalFixedFiles++;
|
|
766
|
+
} else {
|
|
767
|
+
results.push({ file, status: 'no-change' });
|
|
768
|
+
}
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
|
|
771
|
+
results.push({ file, status: 'error', error: error.message });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
console.log(chalk.blue(`\n📊 Summary: Cleaned duplicate roles in ${totalFixedFiles} files`));
|
|
776
|
+
return results;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
cleanupDuplicateRolesInContent(content) {
|
|
780
|
+
let fixed = content;
|
|
781
|
+
let changesMade = false;
|
|
782
|
+
|
|
783
|
+
// Pattern to match duplicate role attributes
|
|
784
|
+
// Matches: role="value" role="value" or role="value" role="value" role="value" etc.
|
|
785
|
+
const duplicateRolePattern = /(\s+role\s*=\s*["']([^"']+)["'])(\s+role\s*=\s*["']\2["'])+/gi;
|
|
786
|
+
|
|
787
|
+
fixed = fixed.replace(duplicateRolePattern, (match, firstRole, roleValue) => {
|
|
788
|
+
changesMade = true;
|
|
789
|
+
console.log(chalk.yellow(` 🧹 Removed duplicate role="${roleValue}" attributes`));
|
|
790
|
+
return firstRole; // Keep only the first occurrence
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Also handle cases where roles have different quotes but same value
|
|
794
|
+
// e.g., role="button" role='button'
|
|
795
|
+
const mixedQuotePattern = /(\s+role\s*=\s*["']([^"']+)["'])(\s+role\s*=\s*['"]?\2['"]?)+/gi;
|
|
796
|
+
|
|
797
|
+
fixed = fixed.replace(mixedQuotePattern, (match, firstRole, roleValue) => {
|
|
798
|
+
if (!changesMade) { // Only log if not already logged above
|
|
799
|
+
changesMade = true;
|
|
800
|
+
console.log(chalk.yellow(` 🧹 Removed duplicate role="${roleValue}" attributes (mixed quotes)`));
|
|
801
|
+
}
|
|
802
|
+
return firstRole;
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
return fixed;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async fixAllAccessibilityIssues(directory = '.') {
|
|
809
|
+
console.log(chalk.blue('🚀 Starting comprehensive accessibility fixes...'));
|
|
810
|
+
|
|
811
|
+
const results = {
|
|
812
|
+
lang: [],
|
|
813
|
+
alt: [],
|
|
814
|
+
roles: [],
|
|
815
|
+
cleanup: []
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
// Step 1: Fix lang attributes
|
|
820
|
+
console.log(chalk.yellow('\n📝 Step 1: HTML lang attributes...'));
|
|
821
|
+
results.lang = await this.fixHtmlLang(directory);
|
|
822
|
+
|
|
823
|
+
// Step 2: Fix alt attributes
|
|
824
|
+
console.log(chalk.yellow('\n🖼️ Step 2: Alt attributes...'));
|
|
825
|
+
results.alt = await this.fixEmptyAltAttributes(directory);
|
|
826
|
+
|
|
827
|
+
// Step 3: Fix role attributes
|
|
828
|
+
console.log(chalk.yellow('\n🎭 Step 3: Role attributes...'));
|
|
829
|
+
results.roles = await this.fixRoleAttributes(directory);
|
|
830
|
+
|
|
831
|
+
// Step 4: Cleanup duplicate roles
|
|
832
|
+
console.log(chalk.yellow('\n🧹 Step 4: Cleanup duplicate roles...'));
|
|
833
|
+
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
834
|
+
|
|
835
|
+
// Summary
|
|
836
|
+
const totalFiles = new Set([
|
|
837
|
+
...results.lang.map(r => r.file),
|
|
838
|
+
...results.alt.map(r => r.file),
|
|
839
|
+
...results.roles.map(r => r.file),
|
|
840
|
+
...results.cleanup.map(r => r.file)
|
|
841
|
+
]).size;
|
|
842
|
+
|
|
843
|
+
const totalFixed = new Set([
|
|
844
|
+
...results.lang.filter(r => r.status === 'fixed').map(r => r.file),
|
|
845
|
+
...results.alt.filter(r => r.status === 'fixed').map(r => r.file),
|
|
846
|
+
...results.roles.filter(r => r.status === 'fixed').map(r => r.file),
|
|
847
|
+
...results.cleanup.filter(r => r.status === 'fixed').map(r => r.file)
|
|
848
|
+
]).size;
|
|
849
|
+
|
|
850
|
+
const totalIssues =
|
|
851
|
+
results.lang.filter(r => r.status === 'fixed').length +
|
|
852
|
+
results.alt.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
853
|
+
results.roles.reduce((sum, r) => sum + (r.issues || 0), 0) +
|
|
854
|
+
results.cleanup.filter(r => r.status === 'fixed').length;
|
|
855
|
+
|
|
856
|
+
console.log(chalk.green('\n🎉 All accessibility fixes completed!'));
|
|
857
|
+
console.log(chalk.blue('📊 Final Summary:'));
|
|
858
|
+
console.log(chalk.white(` Total files scanned: ${totalFiles}`));
|
|
859
|
+
console.log(chalk.green(` Files fixed: ${totalFixed}`));
|
|
860
|
+
console.log(chalk.yellow(` Total issues resolved: ${totalIssues}`));
|
|
861
|
+
|
|
862
|
+
if (this.config.dryRun) {
|
|
863
|
+
console.log(chalk.cyan('\n💡 This was a dry run. Use without --dry-run to apply changes.'));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return results;
|
|
867
|
+
|
|
868
|
+
} catch (error) {
|
|
869
|
+
console.error(chalk.red('❌ Error during comprehensive fix:'), error.message);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
742
874
|
async findHtmlFiles(directory) {
|
|
743
875
|
const files = [];
|
|
744
876
|
|
|
877
|
+
// Check if the path is a file or directory
|
|
878
|
+
const stat = await fs.stat(directory);
|
|
879
|
+
|
|
880
|
+
if (stat.isFile()) {
|
|
881
|
+
// If it's a file, check if it's HTML
|
|
882
|
+
if (directory.endsWith('.html')) {
|
|
883
|
+
files.push(directory);
|
|
884
|
+
}
|
|
885
|
+
return files;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// If it's a directory, scan recursively
|
|
745
889
|
async function scan(dir) {
|
|
746
890
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
747
891
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbu-accessibility-package",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Automated accessibility fixes for HTML files - Alt attributes, Lang attributes, Role attributes. Smart context-aware alt text generation and comprehensive role attribute management.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|