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 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. addMainLandmarks(directory)
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.0.0",
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": {