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 +15 -16
- package/cli.js +174 -77
- package/demo/advanced-test.html +44 -0
- package/demo/advanced-test.html.backup +44 -0
- package/demo/comprehensive-test.html +21 -0
- package/demo/comprehensive-test.html.backup +21 -0
- package/lib/fixer.js +678 -3
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ npm install gbu-accessibility-package
|
|
|
33
33
|
### Basic Usage
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
#
|
|
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
|
|
62
|
+
--comprehensive, --all Run comprehensive fixes (same as default)
|
|
66
63
|
--cleanup-only Only cleanup duplicate role attributes
|
|
67
|
-
--alt-only
|
|
68
|
-
--lang-only
|
|
69
|
-
--role-only
|
|
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
|
|
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 #
|
|
87
|
-
gbu-a11y --lang-only #
|
|
88
|
-
gbu-a11y --role-only #
|
|
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
|
|
93
|
-
gbu-a11y --role-only -l en ./public #
|
|
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
|
|
109
|
+
--comprehensive, --all Run comprehensive fixes (same as default)
|
|
90
110
|
--cleanup-only Only cleanup duplicate role attributes
|
|
91
|
-
--alt-only
|
|
92
|
-
--lang-only
|
|
93
|
-
--role-only
|
|
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 #
|
|
98
|
-
node cli.js --comprehensive #
|
|
99
|
-
node cli.js --alt-only #
|
|
100
|
-
node cli.js --
|
|
101
|
-
node cli.js --
|
|
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
|
|
105
|
-
node cli.js --no-backup ./public #
|
|
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.
|
|
152
|
-
|
|
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
|
-
//
|
|
172
|
-
console.log(chalk.blue('š¼ļø Running alt attribute fixes
|
|
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
|
-
|
|
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
|
-
//
|
|
184
|
-
console.log(chalk.blue('š Running HTML lang attribute fixes
|
|
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
|
-
|
|
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
|
-
//
|
|
195
|
-
console.log(chalk.blue('š Running role attribute fixes
|
|
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
|
-
|
|
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:
|
|
958
|
-
console.log(chalk.yellow('\n
|
|
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": "
|
|
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",
|