gbu-accessibility-package 1.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ npm install gbu-accessibility-package
33
33
  ### Basic Usage
34
34
 
35
35
  ```bash
36
- # Fix current directory
36
+ # Comprehensive fixes (default mode)
37
37
  gbu-a11y
38
38
 
39
39
  # Preview changes (dry run)
@@ -42,9 +42,6 @@ gbu-a11y --dry-run
42
42
  # Fix specific directory
43
43
  gbu-a11y ./src
44
44
 
45
- # Comprehensive fixes (recommended)
46
- gbu-a11y --comprehensive
47
-
48
45
  # Fix specific file
49
46
  gbu-a11y index.html
50
47
  ```
@@ -62,11 +59,11 @@ Options:
62
59
  --backup Create backup files (default: enabled)
63
60
  --no-backup Don't create backup files
64
61
  --dry-run Preview changes without applying
65
- --comprehensive, --all Run all fixes including cleanup (recommended)
62
+ --comprehensive, --all Run comprehensive fixes (same as default)
66
63
  --cleanup-only Only cleanup duplicate role attributes
67
- --alt-only Only fix alt attributes for images
68
- --lang-only Only fix HTML lang attributes
69
- --role-only Only fix role attributes
64
+ --alt-only Fix alt attributes + cleanup
65
+ --lang-only Fix HTML lang attributes + cleanup
66
+ --role-only Fix role attributes + cleanup
70
67
  -h, --help Show help message
71
68
  ```
72
69
 
@@ -76,21 +73,21 @@ Options:
76
73
  # Basic fixes for current directory (all standard fixes)
77
74
  gbu-a11y
78
75
 
79
- # Preview all changes
80
- gbu-a11y --dry-run --comprehensive
76
+ # Preview all changes (comprehensive by default)
77
+ gbu-a11y --dry-run
81
78
 
82
79
  # Fix with English language
83
80
  gbu-a11y -l en ./public
84
81
 
85
- # Individual fix types
86
- gbu-a11y --alt-only # Only fix alt attributes
87
- gbu-a11y --lang-only # Only fix lang attributes
88
- gbu-a11y --role-only # Only fix role attributes
82
+ # Individual fix types (all include cleanup)
83
+ gbu-a11y --alt-only # Fix alt attributes + cleanup
84
+ gbu-a11y --lang-only # Fix lang attributes + cleanup
85
+ gbu-a11y --role-only # Fix role attributes + cleanup
89
86
  gbu-a11y --cleanup-only # Only cleanup duplicates
90
87
 
91
88
  # Combine with other options
92
- gbu-a11y --alt-only --dry-run ./src # Preview alt fixes only
93
- gbu-a11y --role-only -l en ./public # Fix roles with English lang
89
+ gbu-a11y --alt-only --dry-run ./src # Preview alt fixes + cleanup
90
+ gbu-a11y --role-only -l en ./public # Role fixes + cleanup with English lang
94
91
 
95
92
  # Backup options
96
93
  gbu-a11y --backup ./dist # Explicitly enable backups (default)
@@ -308,6 +305,7 @@ The package uses intelligent context analysis to generate meaningful alt text:
308
305
  ## šŸ”’ Safety Features
309
306
 
310
307
  ### Backup Options
308
+
311
309
  - **Default behavior**: Creates `.backup` files automatically for safety
312
310
  - **Disable backups**: Use `--no-backup` for faster processing
313
311
  - **Explicit enable**: Use `--backup` to be explicit about backup creation
@@ -324,6 +322,7 @@ gbu-a11y --backup --comprehensive
324
322
  ```
325
323
 
326
324
  ### Other Safety Features
325
+
327
326
  - **Dry run mode** for safe previewing with `--dry-run`
328
327
  - **Non-destructive** - only adds missing attributes
329
328
  - **Duplicate prevention** - won't add existing attributes
package/cli.js CHANGED
@@ -18,10 +18,15 @@ const options = {
18
18
  dryRun: false,
19
19
  help: false,
20
20
  cleanupOnly: false,
21
- comprehensive: false,
21
+ comprehensive: false, // Keep for backward compatibility
22
22
  altOnly: false,
23
23
  langOnly: false,
24
- roleOnly: false
24
+ roleOnly: false,
25
+ formsOnly: false,
26
+ buttonsOnly: false,
27
+ linksOnly: false,
28
+ landmarksOnly: false,
29
+ headingsOnly: false
25
30
  };
26
31
 
27
32
  // Parse arguments
@@ -55,7 +60,7 @@ for (let i = 0; i < args.length; i++) {
55
60
  break;
56
61
  case '--comprehensive':
57
62
  case '--all':
58
- options.comprehensive = true;
63
+ options.comprehensive = true; // Keep for backward compatibility
59
64
  break;
60
65
  case '--alt-only':
61
66
  options.altOnly = true;
@@ -66,6 +71,21 @@ for (let i = 0; i < args.length; i++) {
66
71
  case '--role-only':
67
72
  options.roleOnly = true;
68
73
  break;
74
+ case '--forms-only':
75
+ options.formsOnly = true;
76
+ break;
77
+ case '--buttons-only':
78
+ options.buttonsOnly = true;
79
+ break;
80
+ case '--links-only':
81
+ options.linksOnly = true;
82
+ break;
83
+ case '--landmarks-only':
84
+ options.landmarksOnly = true;
85
+ break;
86
+ case '--headings-only':
87
+ options.headingsOnly = true;
88
+ break;
69
89
  default:
70
90
  if (!arg.startsWith('-')) {
71
91
  options.directory = arg;
@@ -86,24 +106,31 @@ Options:
86
106
  --backup Create backup files (default: enabled)
87
107
  --no-backup Don't create backup files
88
108
  --dry-run Preview changes without applying
89
- --comprehensive, --all Run all fixes including cleanup (recommended)
109
+ --comprehensive, --all Run comprehensive fixes (same as default)
90
110
  --cleanup-only Only cleanup duplicate role attributes
91
- --alt-only Only fix alt attributes for images
92
- --lang-only Only fix HTML lang attributes
93
- --role-only Only fix role attributes
111
+ --alt-only Fix alt attributes + cleanup
112
+ --lang-only Fix HTML lang attributes + cleanup
113
+ --role-only Fix role attributes + cleanup
114
+ --forms-only Fix form labels + cleanup
115
+ --buttons-only Fix button names + cleanup
116
+ --links-only Fix link names + cleanup
117
+ --landmarks-only Fix landmarks + cleanup
118
+ --headings-only Analyze heading structure (no auto-fix)
94
119
  -h, --help Show this help message
95
120
 
96
121
  Examples:
97
- node cli.js # Fix current directory (with backups)
98
- node cli.js --comprehensive # Run all fixes including cleanup
99
- node cli.js --alt-only # Only fix alt attributes
100
- node cli.js --lang-only # Only fix lang attributes
101
- node cli.js --role-only # Only fix role attributes
122
+ node cli.js # Comprehensive fixes (default mode)
123
+ node cli.js --comprehensive # Comprehensive fixes (same as default)
124
+ node cli.js --alt-only # Fix alt attributes + cleanup
125
+ node cli.js --forms-only # Fix form labels + cleanup
126
+ node cli.js --buttons-only # Fix button names + cleanup
127
+ node cli.js --links-only # Fix link names + cleanup
128
+ node cli.js --landmarks-only # Fix landmarks + cleanup
129
+ node cli.js --headings-only # Analyze heading structure only
102
130
  node cli.js --cleanup-only # Only cleanup duplicate roles
103
- node cli.js ./src # Fix src directory
104
- node cli.js -l en --dry-run ./dist # Preview fixes for dist directory in English
105
- node cli.js --no-backup ./public # Fix without creating backups
106
- node cli.js --backup --comprehensive # Explicitly enable backups with all fixes
131
+ node cli.js ./src # Fix src directory (comprehensive)
132
+ node cli.js -l en --dry-run ./dist # Preview comprehensive fixes in English
133
+ node cli.js --no-backup ./public # Comprehensive fixes without backups
107
134
 
108
135
  Features:
109
136
  āœ… Alt attributes for images
@@ -115,6 +142,7 @@ Features:
115
142
  process.exit(0);
116
143
  }
117
144
 
145
+
118
146
  // Helper function to show completion message with backup info
119
147
  function showCompletionMessage(options, mode = 'fixes') {
120
148
  if (options.dryRun) {
@@ -147,14 +175,30 @@ async function main() {
147
175
  });
148
176
 
149
177
  try {
150
- // Handle different modes
151
- if (options.comprehensive) {
152
- // Run comprehensive fix (all fixes including cleanup)
178
+ // Handle different modes - All modes now include cleanup
179
+ if (options.cleanupOnly || options.altOnly || options.langOnly || options.roleOnly ||
180
+ options.formsOnly || options.buttonsOnly || options.linksOnly || options.landmarksOnly || options.headingsOnly) {
181
+ // Individual modes - handle each separately, then run cleanup
182
+ } else {
183
+ // Default mode: Run comprehensive fix (all fixes including cleanup)
153
184
  console.log(chalk.blue('šŸŽÆ Running comprehensive accessibility fixes...'));
154
185
  const results = await fixer.fixAllAccessibilityIssues(options.directory);
155
186
 
156
187
  // Results already logged in the method
157
188
  return;
189
+ }
190
+
191
+ // Individual modes
192
+ if (options.cleanupOnly) {
193
+ // Only cleanup duplicate roles
194
+ console.log(chalk.blue('🧹 Running cleanup for duplicate role attributes...'));
195
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
196
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
197
+
198
+ console.log(chalk.green(`\nāœ… Cleaned duplicate roles in ${cleanupFixed} files`));
199
+
200
+ showCompletionMessage(options, 'Cleanup');
201
+ return;
158
202
 
159
203
  } else if (options.cleanupOnly) {
160
204
  // Only cleanup duplicate roles
@@ -168,90 +212,143 @@ async function main() {
168
212
  return;
169
213
 
170
214
  } else if (options.altOnly) {
171
- // Only fix alt attributes
172
- console.log(chalk.blue('šŸ–¼ļø Running alt attribute fixes only...'));
215
+ // Fix alt attributes + cleanup
216
+ console.log(chalk.blue('šŸ–¼ļø Running alt attribute fixes + cleanup...'));
173
217
  const altResults = await fixer.fixEmptyAltAttributes(options.directory);
174
218
  const altFixed = altResults.filter(r => r.status === 'fixed').length;
175
219
  const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
176
220
 
177
221
  console.log(chalk.green(`\nāœ… Fixed alt attributes in ${altFixed} files (${totalAltIssues} issues)`));
178
222
 
179
- showCompletionMessage(options, 'Alt attribute fixes');
223
+ // Run cleanup
224
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
225
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
226
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
227
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
228
+
229
+ showCompletionMessage(options, 'Alt attribute fixes + cleanup');
180
230
  return;
181
231
 
182
232
  } else if (options.langOnly) {
183
- // Only fix lang attributes
184
- console.log(chalk.blue('šŸ“ Running HTML lang attribute fixes only...'));
233
+ // Fix lang attributes + cleanup
234
+ console.log(chalk.blue('šŸ“ Running HTML lang attribute fixes + cleanup...'));
185
235
  const langResults = await fixer.fixHtmlLang(options.directory);
186
236
  const langFixed = langResults.filter(r => r.status === 'fixed').length;
187
237
 
188
238
  console.log(chalk.green(`\nāœ… Fixed lang attributes in ${langFixed} files`));
189
239
 
190
- showCompletionMessage(options, 'Lang attribute fixes');
240
+ // Run cleanup
241
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
242
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
243
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
244
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
245
+
246
+ showCompletionMessage(options, 'Lang attribute fixes + cleanup');
191
247
  return;
192
248
 
193
249
  } else if (options.roleOnly) {
194
- // Only fix role attributes
195
- console.log(chalk.blue('šŸŽ­ Running role attribute fixes only...'));
250
+ // Fix role attributes + cleanup
251
+ console.log(chalk.blue('šŸŽ­ Running role attribute fixes + cleanup...'));
196
252
  const roleResults = await fixer.fixRoleAttributes(options.directory);
197
253
  const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
198
254
  const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
199
255
 
200
256
  console.log(chalk.green(`\nāœ… Fixed role attributes in ${roleFixed} files (${totalRoleIssues} issues)`));
201
257
 
202
- showCompletionMessage(options, 'Role attribute fixes');
258
+ // Run cleanup
259
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
260
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
261
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
262
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
263
+
264
+ showCompletionMessage(options, 'Role attribute fixes + cleanup');
265
+ return;
266
+
267
+ } else if (options.formsOnly) {
268
+ // Fix form labels + cleanup
269
+ console.log(chalk.blue('šŸ“‹ Running form label fixes + cleanup...'));
270
+ const formResults = await fixer.fixFormLabels(options.directory);
271
+ const formFixed = formResults.filter(r => r.status === 'fixed').length;
272
+ const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
273
+
274
+ console.log(chalk.green(`\nāœ… Fixed form labels in ${formFixed} files (${totalFormIssues} issues)`));
275
+
276
+ // Run cleanup
277
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
278
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
279
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
280
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
281
+
282
+ showCompletionMessage(options, 'Form label fixes + cleanup');
283
+ return;
284
+
285
+ } else if (options.buttonsOnly) {
286
+ // Fix button names + cleanup
287
+ console.log(chalk.blue('šŸ”˜ Running button name fixes + cleanup...'));
288
+ const buttonResults = await fixer.fixButtonNames(options.directory);
289
+ const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
290
+ const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
291
+
292
+ console.log(chalk.green(`\nāœ… Fixed button names in ${buttonFixed} files (${totalButtonIssues} issues)`));
293
+
294
+ // Run cleanup
295
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
296
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
297
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
298
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
299
+
300
+ showCompletionMessage(options, 'Button name fixes + cleanup');
301
+ return;
302
+
303
+ } else if (options.linksOnly) {
304
+ // Fix link names + cleanup
305
+ console.log(chalk.blue('šŸ”— Running link name fixes + cleanup...'));
306
+ const linkResults = await fixer.fixLinkNames(options.directory);
307
+ const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
308
+ const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
309
+
310
+ console.log(chalk.green(`\nāœ… Fixed link names in ${linkFixed} files (${totalLinkIssues} issues)`));
311
+
312
+ // Run cleanup
313
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
314
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
315
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
316
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
317
+
318
+ showCompletionMessage(options, 'Link name fixes + cleanup');
319
+ return;
320
+
321
+ } else if (options.landmarksOnly) {
322
+ // Fix landmarks + cleanup
323
+ console.log(chalk.blue('šŸ›ļø Running landmark fixes + cleanup...'));
324
+ const landmarkResults = await fixer.fixLandmarks(options.directory);
325
+ const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
326
+ const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
327
+
328
+ console.log(chalk.green(`\nāœ… Fixed landmarks in ${landmarkFixed} files (${totalLandmarkIssues} issues)`));
329
+
330
+ // Run cleanup
331
+ console.log(chalk.blue('\n🧹 Running cleanup for duplicate role attributes...'));
332
+ const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
333
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
334
+ console.log(chalk.green(`āœ… Cleaned duplicate roles in ${cleanupFixed} files`));
335
+
336
+ showCompletionMessage(options, 'Landmark fixes + cleanup');
337
+ return;
338
+
339
+ } else if (options.headingsOnly) {
340
+ // Analyze headings only (no fixes, no cleanup)
341
+ console.log(chalk.blue('šŸ“‘ Running heading analysis only...'));
342
+ const headingResults = await fixer.analyzeHeadings(options.directory);
343
+ const totalSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
344
+
345
+ console.log(chalk.green(`\nāœ… Analyzed headings in ${headingResults.length} files (${totalSuggestions} suggestions)`));
346
+ console.log(chalk.gray('šŸ’” Heading issues require manual review and cannot be auto-fixed'));
347
+
348
+ showCompletionMessage(options, 'Heading analysis');
203
349
  return;
204
350
  }
205
351
 
206
- // Standard mode - run individual fixes
207
- // Fix HTML lang attributes
208
- console.log(chalk.yellow('šŸ“ Step 1: Fixing HTML lang attributes...'));
209
- const langResults = await fixer.fixHtmlLang(options.directory);
210
- const langFixed = langResults.filter(r => r.status === 'fixed').length;
211
- console.log(chalk.green(`āœ… Fixed lang attributes in ${langFixed} files`));
212
- console.log('');
213
-
214
- // Fix alt attributes
215
- console.log(chalk.yellow('šŸ–¼ļø Step 2: Fixing alt attributes...'));
216
- const altResults = await fixer.fixEmptyAltAttributes(options.directory);
217
- const altFixed = altResults.filter(r => r.status === 'fixed').length;
218
- const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
219
- console.log(chalk.green(`āœ… Fixed alt attributes in ${altFixed} files (${totalAltIssues} issues)`));
220
- console.log('');
221
-
222
- // Fix role attributes
223
- console.log(chalk.yellow('šŸŽ­ Step 3: Fixing role attributes...'));
224
- const roleResults = await fixer.fixRoleAttributes(options.directory);
225
- const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
226
- const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
227
- console.log(chalk.green(`āœ… Fixed role attributes in ${roleFixed} files (${totalRoleIssues} issues)`));
228
- console.log('');
229
-
230
- // Summary
231
- const totalFiles = new Set([
232
- ...langResults.map(r => r.file),
233
- ...altResults.map(r => r.file),
234
- ...roleResults.map(r => r.file)
235
- ]).size;
236
-
237
- const totalFixed = new Set([
238
- ...langResults.filter(r => r.status === 'fixed').map(r => r.file),
239
- ...altResults.filter(r => r.status === 'fixed').map(r => r.file),
240
- ...roleResults.filter(r => r.status === 'fixed').map(r => r.file)
241
- ]).size;
242
-
243
- const totalIssues = totalAltIssues + totalRoleIssues + langFixed;
244
-
245
- console.log(chalk.blue('šŸ“Š Summary:'));
246
- console.log(chalk.white(` Total files scanned: ${totalFiles}`));
247
- console.log(chalk.green(` Files fixed: ${totalFixed}`));
248
- console.log(chalk.yellow(` Total issues resolved: ${totalIssues}`));
249
-
250
- showCompletionMessage(options, 'All accessibility fixes');
251
-
252
- // Suggest cleanup if not comprehensive mode
253
- console.log(chalk.blue('\nšŸ’” Pro tip: Use --comprehensive to include duplicate role cleanup!'));
254
-
255
352
  } catch (error) {
256
353
  console.error(chalk.red('āŒ Error occurred:'), error.message);
257
354
  process.exit(1);
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Advanced Accessibility Test</title>
6
+ </head>
7
+ <body>
8
+ <h1>Advanced Accessibility Test</h1>
9
+
10
+ <!-- Form issues -->
11
+ <form>
12
+ <input type="text" placeholder="Name">
13
+ <input type="email" id="email">
14
+ <input type="password">
15
+ <input type="submit">
16
+ </form>
17
+
18
+ <!-- Button issues -->
19
+ <button></button>
20
+ <button onclick="alert('test')"></button>
21
+ <input type="button">
22
+
23
+ <!-- Link issues -->
24
+ <a href="/home"></a>
25
+ <a href="/more">Click here</a>
26
+ <a href="/read">Read more</a>
27
+ <a href="/image"><img src="icon.png"></a>
28
+
29
+ <!-- Heading issues -->
30
+ <h3>Skipped h2</h3>
31
+ <h1>Second h1</h1>
32
+ <h4></h4>
33
+
34
+ <!-- Landmark issues -->
35
+ <div class="content">
36
+ <p>Main content without landmark</p>
37
+ </div>
38
+
39
+ <ul class="navigation">
40
+ <li><a href="/home">Home</a></li>
41
+ <li><a href="/about">About</a></li>
42
+ </ul>
43
+ </body>
44
+ </html>
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Advanced Accessibility Test</title>
6
+ </head>
7
+ <body>
8
+ <h1>Advanced Accessibility Test</h1>
9
+
10
+ <!-- Form issues -->
11
+ <form>
12
+ <input type="text" placeholder="Name">
13
+ <input type="email" id="email">
14
+ <input type="password">
15
+ <input type="submit">
16
+ </form>
17
+
18
+ <!-- Button issues -->
19
+ <button></button>
20
+ <button onclick="alert('test')"></button>
21
+ <input type="button">
22
+
23
+ <!-- Link issues -->
24
+ <a href="/home"></a>
25
+ <a href="/more">Click here</a>
26
+ <a href="/read">Read more</a>
27
+ <a href="/image"><img src="icon.png"></a>
28
+
29
+ <!-- Heading issues -->
30
+ <h3>Skipped h2</h3>
31
+ <h1>Second h1</h1>
32
+ <h4></h4>
33
+
34
+ <!-- Landmark issues -->
35
+ <div class="content">
36
+ <p>Main content without landmark</p>
37
+ </div>
38
+
39
+ <ul class="navigation">
40
+ <li><a href="/home">Home</a></li>
41
+ <li><a href="/about">About</a></li>
42
+ </ul>
43
+ </body>
44
+ </html>
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Comprehensive Test</title>
6
+ </head>
7
+ <body>
8
+ <h1>Comprehensive Test File</h1>
9
+
10
+ <!-- Missing alt -->
11
+ <img src="test.jpg">
12
+
13
+ <!-- Missing role -->
14
+ <a href="/home">Home</a>
15
+
16
+ <!-- Duplicate roles -->
17
+ <img src="dup.jpg" alt="Duplicate" role="img" role="img">
18
+
19
+ <p>This tests comprehensive mode as default.</p>
20
+ </body>
21
+ </html>
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Comprehensive Test</title>
6
+ </head>
7
+ <body>
8
+ <h1>Comprehensive Test File</h1>
9
+
10
+ <!-- Missing alt -->
11
+ <img src="test.jpg">
12
+
13
+ <!-- Missing role -->
14
+ <a href="/home">Home</a>
15
+
16
+ <!-- Duplicate roles -->
17
+ <img src="dup.jpg" alt="Duplicate" role="img" role="img">
18
+
19
+ <p>This tests comprehensive mode as default.</p>
20
+ </body>
21
+ </html>
package/lib/fixer.js CHANGED
@@ -938,7 +938,12 @@ class AccessibilityFixer {
938
938
  lang: [],
939
939
  alt: [],
940
940
  roles: [],
941
- cleanup: []
941
+ cleanup: [],
942
+ forms: [],
943
+ buttons: [],
944
+ links: [],
945
+ landmarks: [],
946
+ headings: [] // Analysis only
942
947
  };
943
948
 
944
949
  try {
@@ -954,8 +959,28 @@ class AccessibilityFixer {
954
959
  console.log(chalk.yellow('\nšŸŽ­ Step 3: Role attributes...'));
955
960
  results.roles = await this.fixRoleAttributes(directory);
956
961
 
957
- // Step 4: Cleanup duplicate roles
958
- console.log(chalk.yellow('\n🧹 Step 4: Cleanup duplicate roles...'));
962
+ // Step 4: Fix form labels
963
+ console.log(chalk.yellow('\nšŸ“‹ Step 4: Form labels...'));
964
+ results.forms = await this.fixFormLabels(directory);
965
+
966
+ // Step 5: Fix button names
967
+ console.log(chalk.yellow('\nšŸ”˜ Step 5: Button names...'));
968
+ results.buttons = await this.fixButtonNames(directory);
969
+
970
+ // Step 6: Fix link names
971
+ console.log(chalk.yellow('\nšŸ”— Step 6: Link names...'));
972
+ results.links = await this.fixLinkNames(directory);
973
+
974
+ // Step 7: Fix landmarks
975
+ console.log(chalk.yellow('\nšŸ›ļø Step 7: Landmarks...'));
976
+ results.landmarks = await this.fixLandmarks(directory);
977
+
978
+ // Step 8: Analyze headings (no auto-fix)
979
+ console.log(chalk.yellow('\nšŸ“‘ Step 8: Heading analysis...'));
980
+ results.headings = await this.analyzeHeadings(directory);
981
+
982
+ // Step 9: Cleanup duplicate roles
983
+ console.log(chalk.yellow('\n🧹 Step 9: Cleanup duplicate roles...'));
959
984
  results.cleanup = await this.cleanupDuplicateRoles(directory);
960
985
 
961
986
  // Summary
@@ -963,6 +988,10 @@ class AccessibilityFixer {
963
988
  ...results.lang.map(r => r.file),
964
989
  ...results.alt.map(r => r.file),
965
990
  ...results.roles.map(r => r.file),
991
+ ...results.forms.map(r => r.file),
992
+ ...results.buttons.map(r => r.file),
993
+ ...results.links.map(r => r.file),
994
+ ...results.landmarks.map(r => r.file),
966
995
  ...results.cleanup.map(r => r.file)
967
996
  ]).size;
968
997
 
@@ -970,6 +999,10 @@ class AccessibilityFixer {
970
999
  ...results.lang.filter(r => r.status === 'fixed').map(r => r.file),
971
1000
  ...results.alt.filter(r => r.status === 'fixed').map(r => r.file),
972
1001
  ...results.roles.filter(r => r.status === 'fixed').map(r => r.file),
1002
+ ...results.forms.filter(r => r.status === 'fixed').map(r => r.file),
1003
+ ...results.buttons.filter(r => r.status === 'fixed').map(r => r.file),
1004
+ ...results.links.filter(r => r.status === 'fixed').map(r => r.file),
1005
+ ...results.landmarks.filter(r => r.status === 'fixed').map(r => r.file),
973
1006
  ...results.cleanup.filter(r => r.status === 'fixed').map(r => r.file)
974
1007
  ]).size;
975
1008
 
@@ -977,6 +1010,10 @@ class AccessibilityFixer {
977
1010
  results.lang.filter(r => r.status === 'fixed').length +
978
1011
  results.alt.reduce((sum, r) => sum + (r.issues || 0), 0) +
979
1012
  results.roles.reduce((sum, r) => sum + (r.issues || 0), 0) +
1013
+ results.forms.reduce((sum, r) => sum + (r.issues || 0), 0) +
1014
+ results.buttons.reduce((sum, r) => sum + (r.issues || 0), 0) +
1015
+ results.links.reduce((sum, r) => sum + (r.issues || 0), 0) +
1016
+ results.landmarks.reduce((sum, r) => sum + (r.issues || 0), 0) +
980
1017
  results.cleanup.filter(r => r.status === 'fixed').length;
981
1018
 
982
1019
  console.log(chalk.green('\nšŸŽ‰ All accessibility fixes completed!'));
@@ -997,6 +1034,644 @@ class AccessibilityFixer {
997
1034
  }
998
1035
  }
999
1036
 
1037
+ // Fix form labels
1038
+ async fixFormLabels(directory = '.') {
1039
+ console.log(chalk.blue('šŸ“‹ Fixing form labels...'));
1040
+
1041
+ const htmlFiles = await this.findHtmlFiles(directory);
1042
+ const results = [];
1043
+ let totalIssuesFound = 0;
1044
+
1045
+ for (const file of htmlFiles) {
1046
+ try {
1047
+ const content = await fs.readFile(file, 'utf8');
1048
+ const issues = this.analyzeFormLabels(content);
1049
+
1050
+ if (issues.length > 0) {
1051
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
1052
+ issues.forEach(issue => {
1053
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
1054
+ totalIssuesFound++;
1055
+ });
1056
+ }
1057
+
1058
+ const fixed = this.fixFormLabelsInContent(content);
1059
+
1060
+ if (fixed !== content) {
1061
+ if (this.config.backupFiles) {
1062
+ await fs.writeFile(`${file}.backup`, content);
1063
+ }
1064
+
1065
+ if (!this.config.dryRun) {
1066
+ await fs.writeFile(file, fixed);
1067
+ }
1068
+
1069
+ console.log(chalk.green(`āœ… Fixed form labels in: ${file}`));
1070
+ results.push({ file, status: 'fixed', issues: issues.length });
1071
+ } else {
1072
+ results.push({ file, status: 'no-change', issues: issues.length });
1073
+ }
1074
+ } catch (error) {
1075
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
1076
+ results.push({ file, status: 'error', error: error.message });
1077
+ }
1078
+ }
1079
+
1080
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} form label issues across ${results.length} files`));
1081
+ return results;
1082
+ }
1083
+
1084
+ analyzeFormLabels(content) {
1085
+ const issues = [];
1086
+
1087
+ // Find input elements without labels
1088
+ const inputPattern = /<input[^>]*>/gi;
1089
+ const inputs = content.match(inputPattern) || [];
1090
+
1091
+ inputs.forEach((input, index) => {
1092
+ const hasId = /id\s*=\s*["']([^"']+)["']/i.test(input);
1093
+ const hasAriaLabel = /aria-label\s*=/i.test(input);
1094
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(input);
1095
+ const inputType = input.match(/type\s*=\s*["']([^"']+)["']/i);
1096
+ const type = inputType ? inputType[1].toLowerCase() : 'text';
1097
+
1098
+ // Skip certain input types that don't need labels
1099
+ if (['hidden', 'submit', 'button', 'reset'].includes(type)) {
1100
+ return;
1101
+ }
1102
+
1103
+ if (hasId) {
1104
+ const idMatch = input.match(/id\s*=\s*["']([^"']+)["']/i);
1105
+ const id = idMatch[1];
1106
+ const labelPattern = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>`, 'i');
1107
+ const hasLabel = labelPattern.test(content);
1108
+
1109
+ if (!hasLabel && !hasAriaLabel && !hasAriaLabelledby) {
1110
+ issues.push({
1111
+ type: 'šŸ“‹ Missing label',
1112
+ description: `Input ${index + 1} (type: ${type}) needs a label or aria-label`,
1113
+ element: input.substring(0, 100) + '...'
1114
+ });
1115
+ }
1116
+ } else if (!hasAriaLabel && !hasAriaLabelledby) {
1117
+ issues.push({
1118
+ type: 'šŸ“‹ Missing label/id',
1119
+ description: `Input ${index + 1} (type: ${type}) needs an id and label, or aria-label`,
1120
+ element: input.substring(0, 100) + '...'
1121
+ });
1122
+ }
1123
+ });
1124
+
1125
+ return issues;
1126
+ }
1127
+
1128
+ fixFormLabelsInContent(content) {
1129
+ let fixed = content;
1130
+
1131
+ // Add aria-label to inputs without labels (basic fix)
1132
+ const inputPattern = /<input([^>]*type\s*=\s*["']([^"']+)["'][^>]*)>/gi;
1133
+
1134
+ fixed = fixed.replace(inputPattern, (match, attributes, type) => {
1135
+ const lowerType = type.toLowerCase();
1136
+
1137
+ // Skip certain input types
1138
+ if (['hidden', 'submit', 'button', 'reset'].includes(lowerType)) {
1139
+ return match;
1140
+ }
1141
+
1142
+ const hasAriaLabel = /aria-label\s*=/i.test(match);
1143
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
1144
+ const hasId = /id\s*=\s*["']([^"']+)["']/i.test(match);
1145
+
1146
+ if (!hasAriaLabel && !hasAriaLabelledby) {
1147
+ // Check if there's a corresponding label
1148
+ if (hasId) {
1149
+ const idMatch = match.match(/id\s*=\s*["']([^"']+)["']/i);
1150
+ const id = idMatch[1];
1151
+ const labelPattern = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>`, 'i');
1152
+
1153
+ if (!labelPattern.test(content)) {
1154
+ // Add basic aria-label
1155
+ const labelText = this.generateInputLabel(lowerType);
1156
+ console.log(chalk.yellow(` šŸ“‹ Added aria-label="${labelText}" to ${lowerType} input`));
1157
+ return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 aria-label="${labelText}"$2`);
1158
+ }
1159
+ } else {
1160
+ // Add basic aria-label
1161
+ const labelText = this.generateInputLabel(lowerType);
1162
+ console.log(chalk.yellow(` šŸ“‹ Added aria-label="${labelText}" to ${lowerType} input`));
1163
+ return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 aria-label="${labelText}"$2`);
1164
+ }
1165
+ }
1166
+
1167
+ return match;
1168
+ });
1169
+
1170
+ return fixed;
1171
+ }
1172
+
1173
+ generateInputLabel(type) {
1174
+ const labels = {
1175
+ 'text': 'ćƒ†ć‚­ć‚¹ćƒˆå…„åŠ›',
1176
+ 'email': 'ćƒ”ćƒ¼ćƒ«ć‚¢ćƒ‰ćƒ¬ć‚¹',
1177
+ 'password': 'ćƒ‘ć‚¹ćƒÆćƒ¼ćƒ‰',
1178
+ 'tel': 'é›»č©±ē•Ŗå·',
1179
+ 'url': 'URL',
1180
+ 'search': '検瓢',
1181
+ 'number': '数値',
1182
+ 'date': 'ę—„ä»˜',
1183
+ 'time': 'Ꙃ間',
1184
+ 'checkbox': 'ćƒć‚§ćƒƒć‚Æćƒœćƒƒć‚Æć‚¹',
1185
+ 'radio': 'ćƒ©ć‚øć‚Ŗćƒœć‚æćƒ³',
1186
+ 'file': 'ćƒ•ć‚”ć‚¤ćƒ«éøęŠž'
1187
+ };
1188
+
1189
+ return labels[type] || 'ćƒ•ć‚©ćƒ¼ćƒ å…„åŠ›';
1190
+ }
1191
+
1192
+ // Fix button names
1193
+ async fixButtonNames(directory = '.') {
1194
+ console.log(chalk.blue('šŸ”˜ Fixing button names...'));
1195
+
1196
+ const htmlFiles = await this.findHtmlFiles(directory);
1197
+ const results = [];
1198
+ let totalIssuesFound = 0;
1199
+
1200
+ for (const file of htmlFiles) {
1201
+ try {
1202
+ const content = await fs.readFile(file, 'utf8');
1203
+ const issues = this.analyzeButtonNames(content);
1204
+
1205
+ if (issues.length > 0) {
1206
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
1207
+ issues.forEach(issue => {
1208
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
1209
+ totalIssuesFound++;
1210
+ });
1211
+ }
1212
+
1213
+ const fixed = this.fixButtonNamesInContent(content);
1214
+
1215
+ if (fixed !== content) {
1216
+ if (this.config.backupFiles) {
1217
+ await fs.writeFile(`${file}.backup`, content);
1218
+ }
1219
+
1220
+ if (!this.config.dryRun) {
1221
+ await fs.writeFile(file, fixed);
1222
+ }
1223
+
1224
+ console.log(chalk.green(`āœ… Fixed button names in: ${file}`));
1225
+ results.push({ file, status: 'fixed', issues: issues.length });
1226
+ } else {
1227
+ results.push({ file, status: 'no-change', issues: issues.length });
1228
+ }
1229
+ } catch (error) {
1230
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
1231
+ results.push({ file, status: 'error', error: error.message });
1232
+ }
1233
+ }
1234
+
1235
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} button name issues across ${results.length} files`));
1236
+ return results;
1237
+ }
1238
+
1239
+ analyzeButtonNames(content) {
1240
+ const issues = [];
1241
+
1242
+ // Find buttons without discernible text
1243
+ const buttonPattern = /<button[^>]*>[\s\S]*?<\/button>/gi;
1244
+ const buttons = content.match(buttonPattern) || [];
1245
+
1246
+ buttons.forEach((button, index) => {
1247
+ const hasAriaLabel = /aria-label\s*=/i.test(button);
1248
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(button);
1249
+ const hasTitle = /title\s*=/i.test(button);
1250
+
1251
+ // Extract text content
1252
+ const textContent = button.replace(/<[^>]*>/g, '').trim();
1253
+
1254
+ if (!textContent && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
1255
+ issues.push({
1256
+ type: 'šŸ”˜ Empty button',
1257
+ description: `Button ${index + 1} has no discernible text`,
1258
+ element: button.substring(0, 100) + '...'
1259
+ });
1260
+ }
1261
+ });
1262
+
1263
+ // Find input buttons without names
1264
+ const inputButtonPattern = /<input[^>]*type\s*=\s*["'](button|submit|reset)["'][^>]*>/gi;
1265
+ const inputButtons = content.match(inputButtonPattern) || [];
1266
+
1267
+ inputButtons.forEach((input, index) => {
1268
+ const hasValue = /value\s*=/i.test(input);
1269
+ const hasAriaLabel = /aria-label\s*=/i.test(input);
1270
+ const hasTitle = /title\s*=/i.test(input);
1271
+
1272
+ if (!hasValue && !hasAriaLabel && !hasTitle) {
1273
+ issues.push({
1274
+ type: 'šŸ”˜ Input button without name',
1275
+ description: `Input button ${index + 1} needs value, aria-label, or title`,
1276
+ element: input.substring(0, 100) + '...'
1277
+ });
1278
+ }
1279
+ });
1280
+
1281
+ return issues;
1282
+ }
1283
+
1284
+ fixButtonNamesInContent(content) {
1285
+ let fixed = content;
1286
+
1287
+ // Fix empty buttons
1288
+ fixed = fixed.replace(/<button([^>]*)>\s*<\/button>/gi, (match, attributes) => {
1289
+ const hasAriaLabel = /aria-label\s*=/i.test(match);
1290
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
1291
+ const hasTitle = /title\s*=/i.test(match);
1292
+
1293
+ if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
1294
+ console.log(chalk.yellow(` šŸ”˜ Added aria-label to empty button`));
1295
+ return `<button${attributes} aria-label="ćƒœć‚æćƒ³">ćƒœć‚æćƒ³</button>`;
1296
+ }
1297
+
1298
+ return match;
1299
+ });
1300
+
1301
+ // Fix input buttons without value
1302
+ fixed = fixed.replace(/<input([^>]*type\s*=\s*["'](button|submit|reset)["'][^>]*)>/gi, (match, attributes, type) => {
1303
+ const hasValue = /value\s*=/i.test(match);
1304
+ const hasAriaLabel = /aria-label\s*=/i.test(match);
1305
+ const hasTitle = /title\s*=/i.test(match);
1306
+
1307
+ if (!hasValue && !hasAriaLabel && !hasTitle) {
1308
+ const buttonText = type === 'submit' ? '送俔' : type === 'reset' ? 'ćƒŖć‚»ćƒƒćƒˆ' : 'ćƒœć‚æćƒ³';
1309
+ console.log(chalk.yellow(` šŸ”˜ Added value="${buttonText}" to input ${type} button`));
1310
+ return match.replace(/(<input[^>]*?)(\s*>)/i, `$1 value="${buttonText}"$2`);
1311
+ }
1312
+
1313
+ return match;
1314
+ });
1315
+
1316
+ return fixed;
1317
+ }
1318
+
1319
+ // Fix link names
1320
+ async fixLinkNames(directory = '.') {
1321
+ console.log(chalk.blue('šŸ”— Fixing link names...'));
1322
+
1323
+ const htmlFiles = await this.findHtmlFiles(directory);
1324
+ const results = [];
1325
+ let totalIssuesFound = 0;
1326
+
1327
+ for (const file of htmlFiles) {
1328
+ try {
1329
+ const content = await fs.readFile(file, 'utf8');
1330
+ const issues = this.analyzeLinkNames(content);
1331
+
1332
+ if (issues.length > 0) {
1333
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
1334
+ issues.forEach(issue => {
1335
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
1336
+ totalIssuesFound++;
1337
+ });
1338
+ }
1339
+
1340
+ const fixed = this.fixLinkNamesInContent(content);
1341
+
1342
+ if (fixed !== content) {
1343
+ if (this.config.backupFiles) {
1344
+ await fs.writeFile(`${file}.backup`, content);
1345
+ }
1346
+
1347
+ if (!this.config.dryRun) {
1348
+ await fs.writeFile(file, fixed);
1349
+ }
1350
+
1351
+ console.log(chalk.green(`āœ… Fixed link names in: ${file}`));
1352
+ results.push({ file, status: 'fixed', issues: issues.length });
1353
+ } else {
1354
+ results.push({ file, status: 'no-change', issues: issues.length });
1355
+ }
1356
+ } catch (error) {
1357
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
1358
+ results.push({ file, status: 'error', error: error.message });
1359
+ }
1360
+ }
1361
+
1362
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} link name issues across ${results.length} files`));
1363
+ return results;
1364
+ }
1365
+
1366
+ analyzeLinkNames(content) {
1367
+ const issues = [];
1368
+
1369
+ // Find links without discernible text
1370
+ const linkPattern = /<a[^>]*href[^>]*>[\s\S]*?<\/a>/gi;
1371
+ const links = content.match(linkPattern) || [];
1372
+
1373
+ links.forEach((link, index) => {
1374
+ const hasAriaLabel = /aria-label\s*=/i.test(link);
1375
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(link);
1376
+ const hasTitle = /title\s*=/i.test(link);
1377
+
1378
+ // Extract text content (excluding images)
1379
+ let textContent = link.replace(/<img[^>]*>/gi, '').replace(/<[^>]*>/g, '').trim();
1380
+
1381
+ // Check for image alt text if link contains only images
1382
+ if (!textContent) {
1383
+ const imgMatch = link.match(/<img[^>]*alt\s*=\s*["']([^"']+)["'][^>]*>/i);
1384
+ if (imgMatch) {
1385
+ textContent = imgMatch[1].trim();
1386
+ }
1387
+ }
1388
+
1389
+ if (!textContent && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
1390
+ issues.push({
1391
+ type: 'šŸ”— Empty link',
1392
+ description: `Link ${index + 1} has no discernible text`,
1393
+ element: link.substring(0, 100) + '...'
1394
+ });
1395
+ } else if (textContent && (textContent.toLowerCase() === 'click here' || textContent.toLowerCase() === 'read more' || textContent.toLowerCase() === 'here')) {
1396
+ issues.push({
1397
+ type: 'šŸ”— Generic link text',
1398
+ description: `Link ${index + 1} has generic text: "${textContent}"`,
1399
+ element: link.substring(0, 100) + '...'
1400
+ });
1401
+ }
1402
+ });
1403
+
1404
+ return issues;
1405
+ }
1406
+
1407
+ fixLinkNamesInContent(content) {
1408
+ let fixed = content;
1409
+
1410
+ // Fix empty links
1411
+ fixed = fixed.replace(/<a([^>]*href[^>]*)>\s*<\/a>/gi, (match, attributes) => {
1412
+ const hasAriaLabel = /aria-label\s*=/i.test(match);
1413
+ const hasAriaLabelledby = /aria-labelledby\s*=/i.test(match);
1414
+ const hasTitle = /title\s*=/i.test(match);
1415
+
1416
+ if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
1417
+ console.log(chalk.yellow(` šŸ”— Added aria-label to empty link`));
1418
+ return `<a${attributes} aria-label="ćƒŖćƒ³ć‚Æ">ćƒŖćƒ³ć‚Æ</a>`;
1419
+ }
1420
+
1421
+ return match;
1422
+ });
1423
+
1424
+ // Fix links with only images but no alt text
1425
+ fixed = fixed.replace(/<a([^>]*href[^>]*)>(\s*<img[^>]*>\s*)<\/a>/gi, (match, attributes, imgTag) => {
1426
+ const hasAriaLabel = /aria-label\s*=/i.test(match);
1427
+ const hasAlt = /alt\s*=/i.test(imgTag);
1428
+
1429
+ if (!hasAriaLabel && !hasAlt) {
1430
+ console.log(chalk.yellow(` šŸ”— Added aria-label to image link`));
1431
+ return `<a${attributes} aria-label="ē”»åƒćƒŖćƒ³ć‚Æ">${imgTag}</a>`;
1432
+ }
1433
+
1434
+ return match;
1435
+ });
1436
+
1437
+ return fixed;
1438
+ }
1439
+
1440
+ // Fix landmarks
1441
+ async fixLandmarks(directory = '.') {
1442
+ console.log(chalk.blue('šŸ›ļø Fixing landmarks...'));
1443
+
1444
+ const htmlFiles = await this.findHtmlFiles(directory);
1445
+ const results = [];
1446
+ let totalIssuesFound = 0;
1447
+
1448
+ for (const file of htmlFiles) {
1449
+ try {
1450
+ const content = await fs.readFile(file, 'utf8');
1451
+ const issues = this.analyzeLandmarks(content);
1452
+
1453
+ if (issues.length > 0) {
1454
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
1455
+ issues.forEach(issue => {
1456
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
1457
+ totalIssuesFound++;
1458
+ });
1459
+ }
1460
+
1461
+ const fixed = this.fixLandmarksInContent(content);
1462
+
1463
+ if (fixed !== content) {
1464
+ if (this.config.backupFiles) {
1465
+ await fs.writeFile(`${file}.backup`, content);
1466
+ }
1467
+
1468
+ if (!this.config.dryRun) {
1469
+ await fs.writeFile(file, fixed);
1470
+ }
1471
+
1472
+ console.log(chalk.green(`āœ… Fixed landmarks in: ${file}`));
1473
+ results.push({ file, status: 'fixed', issues: issues.length });
1474
+ } else {
1475
+ results.push({ file, status: 'no-change', issues: issues.length });
1476
+ }
1477
+ } catch (error) {
1478
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
1479
+ results.push({ file, status: 'error', error: error.message });
1480
+ }
1481
+ }
1482
+
1483
+ console.log(chalk.blue(`\nšŸ“Š Summary: Found ${totalIssuesFound} landmark issues across ${results.length} files`));
1484
+ return results;
1485
+ }
1486
+
1487
+ analyzeLandmarks(content) {
1488
+ const issues = [];
1489
+
1490
+ // Check for main landmark
1491
+ const hasMain = /<main[^>]*>/i.test(content) || /role\s*=\s*["']main["']/i.test(content);
1492
+ if (!hasMain) {
1493
+ issues.push({
1494
+ type: 'šŸ›ļø Missing main landmark',
1495
+ description: 'Page should have a main landmark',
1496
+ suggestion: 'Add <main> element or role="main"'
1497
+ });
1498
+ }
1499
+
1500
+ // Check for multiple main landmarks
1501
+ const mainCount = (content.match(/<main[^>]*>/gi) || []).length +
1502
+ (content.match(/role\s*=\s*["']main["']/gi) || []).length;
1503
+ if (mainCount > 1) {
1504
+ issues.push({
1505
+ type: 'šŸ›ļø Multiple main landmarks',
1506
+ description: `Found ${mainCount} main landmarks, should have only one`,
1507
+ suggestion: 'Keep only one main landmark per page'
1508
+ });
1509
+ }
1510
+
1511
+ // Check for navigation landmarks
1512
+ const hasNav = /<nav[^>]*>/i.test(content) || /role\s*=\s*["']navigation["']/i.test(content);
1513
+ if (!hasNav) {
1514
+ // Look for navigation-like elements
1515
+ const navLikeElements = content.match(/<(?:ul|div)[^>]*class\s*=\s*["'][^"']*(?:nav|menu|navigation)[^"']*["'][^>]*>/gi);
1516
+ if (navLikeElements && navLikeElements.length > 0) {
1517
+ issues.push({
1518
+ type: 'šŸ›ļø Missing navigation landmark',
1519
+ description: 'Navigation elements should have nav tag or role="navigation"',
1520
+ suggestion: 'Use <nav> element or add role="navigation"'
1521
+ });
1522
+ }
1523
+ }
1524
+
1525
+ return issues;
1526
+ }
1527
+
1528
+ fixLandmarksInContent(content) {
1529
+ let fixed = content;
1530
+
1531
+ // Add main landmark if missing (basic implementation)
1532
+ const hasMain = /<main[^>]*>/i.test(content) || /role\s*=\s*["']main["']/i.test(content);
1533
+
1534
+ if (!hasMain) {
1535
+ // Look for content containers that could be main
1536
+ const contentPatterns = [
1537
+ /<div[^>]*class\s*=\s*["'][^"']*(?:content|main|container)[^"']*["'][^>]*>/gi,
1538
+ /<section[^>]*class\s*=\s*["'][^"']*(?:content|main)[^"']*["'][^>]*>/gi
1539
+ ];
1540
+
1541
+ for (const pattern of contentPatterns) {
1542
+ const matches = fixed.match(pattern);
1543
+ if (matches && matches.length === 1) {
1544
+ // Add role="main" to the first suitable container
1545
+ fixed = fixed.replace(pattern, (match) => {
1546
+ if (!/role\s*=/i.test(match)) {
1547
+ console.log(chalk.yellow(` šŸ›ļø Added role="main" to content container`));
1548
+ return match.replace(/(<(?:div|section)[^>]*?)(\s*>)/i, '$1 role="main"$2');
1549
+ }
1550
+ return match;
1551
+ });
1552
+ break;
1553
+ }
1554
+ }
1555
+ }
1556
+
1557
+ // Add navigation role to nav-like elements
1558
+ const navPattern = /<(?:ul|div)([^>]*class\s*=\s*["'][^"']*(?:nav|menu|navigation)[^"']*["'][^>]*)>/gi;
1559
+ fixed = fixed.replace(navPattern, (match, attributes) => {
1560
+ if (!/role\s*=/i.test(match)) {
1561
+ console.log(chalk.yellow(` šŸ›ļø Added role="navigation" to navigation element`));
1562
+ return match.replace(/(<(?:ul|div)[^>]*?)(\s*>)/i, '$1 role="navigation"$2');
1563
+ }
1564
+ return match;
1565
+ });
1566
+
1567
+ return fixed;
1568
+ }
1569
+
1570
+ // Analyze headings (no auto-fix, only suggestions)
1571
+ async analyzeHeadings(directory = '.') {
1572
+ console.log(chalk.blue('šŸ“‘ Analyzing heading structure...'));
1573
+
1574
+ const htmlFiles = await this.findHtmlFiles(directory);
1575
+ const results = [];
1576
+
1577
+ for (const file of htmlFiles) {
1578
+ try {
1579
+ const content = await fs.readFile(file, 'utf8');
1580
+ const issues = this.analyzeHeadingStructure(content);
1581
+
1582
+ if (issues.length > 0) {
1583
+ console.log(chalk.cyan(`\nšŸ“ ${file}:`));
1584
+ issues.forEach(issue => {
1585
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
1586
+ if (issue.suggestion) {
1587
+ console.log(chalk.gray(` šŸ’” ${issue.suggestion}`));
1588
+ }
1589
+ });
1590
+ }
1591
+
1592
+ results.push({ file, status: 'analyzed', issues: issues.length, suggestions: issues });
1593
+ } catch (error) {
1594
+ console.error(chalk.red(`āŒ Error processing ${file}: ${error.message}`));
1595
+ results.push({ file, status: 'error', error: error.message });
1596
+ }
1597
+ }
1598
+
1599
+ console.log(chalk.blue(`\nšŸ“Š Summary: Analyzed heading structure in ${results.length} files`));
1600
+ console.log(chalk.gray('šŸ’” Heading issues require manual review and cannot be auto-fixed'));
1601
+ return results;
1602
+ }
1603
+
1604
+ analyzeHeadingStructure(content) {
1605
+ const issues = [];
1606
+
1607
+ // Extract all headings with their levels and text
1608
+ const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
1609
+ const headings = [];
1610
+ let match;
1611
+
1612
+ while ((match = headingPattern.exec(content)) !== null) {
1613
+ const level = parseInt(match[1]);
1614
+ const text = match[2].replace(/<[^>]*>/g, '').trim();
1615
+ headings.push({ level, text, position: match.index });
1616
+ }
1617
+
1618
+ if (headings.length === 0) {
1619
+ issues.push({
1620
+ type: 'šŸ“‘ No headings found',
1621
+ description: 'Page has no heading elements',
1622
+ suggestion: 'Add heading elements (h1-h6) to structure content'
1623
+ });
1624
+ return issues;
1625
+ }
1626
+
1627
+ // Check for h1
1628
+ const hasH1 = headings.some(h => h.level === 1);
1629
+ if (!hasH1) {
1630
+ issues.push({
1631
+ type: 'šŸ“‘ Missing h1',
1632
+ description: 'Page should have exactly one h1 element',
1633
+ suggestion: 'Add an h1 element as the main page heading'
1634
+ });
1635
+ }
1636
+
1637
+ // Check for multiple h1s
1638
+ const h1Count = headings.filter(h => h.level === 1).length;
1639
+ if (h1Count > 1) {
1640
+ issues.push({
1641
+ type: 'šŸ“‘ Multiple h1 elements',
1642
+ description: `Found ${h1Count} h1 elements, should have only one`,
1643
+ suggestion: 'Use only one h1 per page, use h2-h6 for subheadings'
1644
+ });
1645
+ }
1646
+
1647
+ // Check heading order
1648
+ for (let i = 1; i < headings.length; i++) {
1649
+ const current = headings[i];
1650
+ const previous = headings[i - 1];
1651
+
1652
+ if (current.level > previous.level + 1) {
1653
+ issues.push({
1654
+ type: 'šŸ“‘ Heading level skip',
1655
+ description: `Heading level jumps from h${previous.level} to h${current.level}`,
1656
+ suggestion: `Use h${previous.level + 1} instead of h${current.level}, or add intermediate headings`
1657
+ });
1658
+ }
1659
+ }
1660
+
1661
+ // Check for empty headings
1662
+ headings.forEach((heading, index) => {
1663
+ if (!heading.text) {
1664
+ issues.push({
1665
+ type: 'šŸ“‘ Empty heading',
1666
+ description: `Heading ${index + 1} (h${heading.level}) is empty`,
1667
+ suggestion: 'Add descriptive text to the heading or remove it'
1668
+ });
1669
+ }
1670
+ });
1671
+
1672
+ return issues;
1673
+ }
1674
+
1000
1675
  async findHtmlFiles(directory) {
1001
1676
  const files = [];
1002
1677
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbu-accessibility-package",
3
- "version": "1.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "Automated accessibility fixes for HTML files - Alt attributes, Lang attributes, Role attributes. Smart context-aware alt text generation with individual fix options and comprehensive role attribute management.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,6 +15,11 @@
15
15
  "alt-only": "node cli.js --alt-only",
16
16
  "lang-only": "node cli.js --lang-only",
17
17
  "role-only": "node cli.js --role-only",
18
+ "forms-only": "node cli.js --forms-only",
19
+ "buttons-only": "node cli.js --buttons-only",
20
+ "links-only": "node cli.js --links-only",
21
+ "landmarks-only": "node cli.js --landmarks-only",
22
+ "headings-only": "node cli.js --headings-only",
18
23
  "cleanup-only": "node cli.js --cleanup-only",
19
24
  "no-backup": "node cli.js --no-backup",
20
25
  "cleanup-backups": "find . -name '*.backup' -type f -delete",