gbu-accessibility-package 3.2.1 → 3.3.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/QUICK_START.md CHANGED
@@ -15,6 +15,22 @@ npm install gbu-accessibility-package
15
15
  gbu-a11y
16
16
  ```
17
17
 
18
+ ## 🔄 Cài đặt lại / Cập nhật
19
+
20
+ ```bash
21
+ # Gỡ cài đặt cũ
22
+ npm uninstall -g gbu-accessibility-package
23
+
24
+ # Xóa cache
25
+ npm cache clean --force
26
+
27
+ # Cài đặt phiên bản mới nhất
28
+ npm install -g gbu-accessibility-package@latest
29
+
30
+ # Kiểm tra version
31
+ gbu-a11y --version
32
+ ```
33
+
18
34
  ## 🎯 Sử dụng cơ bản
19
35
 
20
36
  ### Cách 1: CLI (Đơn giản nhất)
@@ -115,9 +131,31 @@ gbu-a11y --dry-run
115
131
 
116
132
  **Lỗi "Cannot find module"**
117
133
  ```bash
134
+ # Cài đặt lại
135
+ npm uninstall -g gbu-accessibility-package
136
+ npm cache clean --force
118
137
  npm install -g gbu-accessibility-package
119
138
  ```
120
139
 
140
+ **Lỗi permission (macOS/Linux)**
141
+ ```bash
142
+ sudo npm install -g gbu-accessibility-package
143
+ ```
144
+
145
+ **Package không update**
146
+ ```bash
147
+ # Force update
148
+ npm cache clean --force
149
+ npm install -g gbu-accessibility-package@latest --force
150
+ ```
151
+
152
+ **Kiểm tra cài đặt**
153
+ ```bash
154
+ which gbu-a11y
155
+ npm list -g gbu-accessibility-package
156
+ gbu-a11y --version
157
+ ```
158
+
121
159
  **Duplicate attributes**
122
160
  - Tool tự động tránh duplicate
123
161
  - Nếu có, chạy lại tool sẽ tự clean up
package/README-vi.md CHANGED
@@ -52,6 +52,47 @@ npm install -g gbu-accessibility-package
52
52
  npm install gbu-accessibility-package
53
53
  ```
54
54
 
55
+ ### Gỡ cài đặt và Cài đặt lại
56
+
57
+ ```bash
58
+ # Gỡ cài đặt package global
59
+ npm uninstall -g gbu-accessibility-package
60
+
61
+ # Gỡ cài đặt package local
62
+ npm uninstall gbu-accessibility-package
63
+
64
+ # Xóa cache npm (khuyến nghị khi có vấn đề)
65
+ npm cache clean --force
66
+
67
+ # Cài đặt lại phiên bản mới nhất
68
+ npm install -g gbu-accessibility-package@latest
69
+
70
+ # Kiểm tra phiên bản đã cài đặt
71
+ npm list -g gbu-accessibility-package
72
+ gbu-a11y --version
73
+
74
+ # Cài đặt phiên bản cụ thể
75
+ npm install -g gbu-accessibility-package@3.2.1
76
+ ```
77
+
78
+ ### Khắc phục sự cố cài đặt
79
+
80
+ ```bash
81
+ # Nếu gặp lỗi permission (macOS/Linux)
82
+ sudo npm install -g gbu-accessibility-package
83
+
84
+ # Nếu gặp lỗi cache
85
+ npm cache clean --force
86
+ npm install -g gbu-accessibility-package --force
87
+
88
+ # Kiểm tra cài đặt
89
+ which gbu-a11y
90
+ gbu-a11y --help
91
+
92
+ # Cập nhật lên phiên bản mới nhất
93
+ npm update -g gbu-accessibility-package
94
+ ```
95
+
55
96
  ### Sử dụng cơ bản
56
97
 
57
98
  ```bash
@@ -277,6 +318,50 @@ console.log("Hoàn thành sửa lỗi với enhanced features:", results);
277
318
  - **URL không hợp lệ** → Phát hiện định dạng URL sai
278
319
  - **Liên kết chậm** → Cảnh báo timeout và phản hồi chậm
279
320
 
321
+ ## 🔧 Quản lý Package
322
+
323
+ ### Kiểm tra thông tin package
324
+
325
+ ```bash
326
+ # Xem version hiện tại
327
+ gbu-a11y --version
328
+ npm list -g gbu-accessibility-package
329
+
330
+ # Xem thông tin package
331
+ npm info gbu-accessibility-package
332
+
333
+ # Kiểm tra package đã cài đặt
334
+ which gbu-a11y
335
+ npm list -g | grep gbu-accessibility-package
336
+ ```
337
+
338
+ ### Cập nhật package
339
+
340
+ ```bash
341
+ # Kiểm tra version mới
342
+ npm outdated -g gbu-accessibility-package
343
+
344
+ # Cập nhật lên version mới nhất
345
+ npm update -g gbu-accessibility-package
346
+
347
+ # Hoặc cài đặt lại version mới
348
+ npm uninstall -g gbu-accessibility-package
349
+ npm install -g gbu-accessibility-package@latest
350
+ ```
351
+
352
+ ### Quản lý cache
353
+
354
+ ```bash
355
+ # Xem cache info
356
+ npm cache verify
357
+
358
+ # Xóa cache (khi có vấn đề)
359
+ npm cache clean --force
360
+
361
+ # Xem cache location
362
+ npm config get cache
363
+ ```
364
+
280
365
  ## 🧪 Kiểm tra và Demo
281
366
 
282
367
  ```bash
package/README.md CHANGED
@@ -49,6 +49,47 @@ npm install -g gbu-accessibility-package
49
49
  npm install gbu-accessibility-package
50
50
  ```
51
51
 
52
+ ### Uninstall and Reinstall
53
+
54
+ ```bash
55
+ # Uninstall global package
56
+ npm uninstall -g gbu-accessibility-package
57
+
58
+ # Uninstall local package
59
+ npm uninstall gbu-accessibility-package
60
+
61
+ # Clear npm cache (recommended when having issues)
62
+ npm cache clean --force
63
+
64
+ # Reinstall latest version
65
+ npm install -g gbu-accessibility-package@latest
66
+
67
+ # Check installed version
68
+ npm list -g gbu-accessibility-package
69
+ gbu-a11y --version
70
+
71
+ # Install specific version
72
+ npm install -g gbu-accessibility-package@3.2.1
73
+ ```
74
+
75
+ ### Installation Troubleshooting
76
+
77
+ ```bash
78
+ # If permission errors (macOS/Linux)
79
+ sudo npm install -g gbu-accessibility-package
80
+
81
+ # If cache issues
82
+ npm cache clean --force
83
+ npm install -g gbu-accessibility-package --force
84
+
85
+ # Verify installation
86
+ which gbu-a11y
87
+ gbu-a11y --help
88
+
89
+ # Update to latest version
90
+ npm update -g gbu-accessibility-package
91
+ ```
92
+
52
93
  ### Basic Usage
53
94
 
54
95
  ```bash
@@ -266,6 +307,47 @@ console.log('Accessibility fixes completed with enhanced features:', results);
266
307
  - **Invalid URLs** → Detect malformed URL formats
267
308
  - **Slow links** → Warn about timeouts and slow responses
268
309
 
310
+ ## 🔧 Package Management
311
+
312
+ ### Check package information
313
+ ```bash
314
+ # Check current version
315
+ gbu-a11y --version
316
+ npm list -g gbu-accessibility-package
317
+
318
+ # View package info
319
+ npm info gbu-accessibility-package
320
+
321
+ # Verify installation
322
+ which gbu-a11y
323
+ npm list -g | grep gbu-accessibility-package
324
+ ```
325
+
326
+ ### Update package
327
+ ```bash
328
+ # Check for new versions
329
+ npm outdated -g gbu-accessibility-package
330
+
331
+ # Update to latest version
332
+ npm update -g gbu-accessibility-package
333
+
334
+ # Or reinstall latest version
335
+ npm uninstall -g gbu-accessibility-package
336
+ npm install -g gbu-accessibility-package@latest
337
+ ```
338
+
339
+ ### Cache management
340
+ ```bash
341
+ # Verify cache
342
+ npm cache verify
343
+
344
+ # Clean cache (when having issues)
345
+ npm cache clean --force
346
+
347
+ # View cache location
348
+ npm config get cache
349
+ ```
350
+
269
351
  ## 🧪 Testing and Demo
270
352
 
271
353
  ```bash
@@ -0,0 +1,87 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Form Labels Test - Accessibility Issues</title>
7
+ </head>
8
+ <body>
9
+ <h1>Form Labels Test Cases</h1>
10
+
11
+ <!-- Test Case 1: Input without any label -->
12
+ <div>
13
+ <input type="text" name="username">
14
+ </div>
15
+
16
+ <!-- Test Case 2: Input with empty label -->
17
+ <div>
18
+ <label for="email"></label>
19
+ <input type="email" id="email" name="email">
20
+ </div>
21
+
22
+ <!-- Test Case 3: Input without aria-label -->
23
+ <div>
24
+ <input type="password" name="password" placeholder="Enter password">
25
+ </div>
26
+
27
+ <!-- Test Case 4: Input with invalid aria-labelledby -->
28
+ <div>
29
+ <input type="tel" name="phone" aria-labelledby="nonexistent-id">
30
+ </div>
31
+
32
+ <!-- Test Case 5: Input without title attribute -->
33
+ <div>
34
+ <input type="url" name="website">
35
+ </div>
36
+
37
+ <!-- Test Case 6: Textarea without proper labeling -->
38
+ <div>
39
+ <textarea name="comments"></textarea>
40
+ </div>
41
+
42
+ <!-- Test Case 7: Select without proper labeling -->
43
+ <div>
44
+ <select name="country">
45
+ <option value="">Choose country</option>
46
+ <option value="jp">Japan</option>
47
+ <option value="us">USA</option>
48
+ </select>
49
+ </div>
50
+
51
+ <!-- Test Case 8: Input with implicit label but empty text -->
52
+ <div>
53
+ <label><input type="checkbox" name="agree"></label>
54
+ </div>
55
+
56
+ <!-- Test Case 9: Input with aria-labelledby pointing to empty element -->
57
+ <div>
58
+ <span id="empty-label"></span>
59
+ <input type="number" name="age" aria-labelledby="empty-label">
60
+ </div>
61
+
62
+ <!-- Test Case 10: Input without role override -->
63
+ <div>
64
+ <input type="range" name="volume" min="0" max="100">
65
+ </div>
66
+
67
+ <!-- Test Case 11: Multiple inputs without proper structure -->
68
+ <form>
69
+ <input type="text" name="firstname">
70
+ <input type="text" name="lastname">
71
+ <input type="email" name="user_email">
72
+ <textarea name="message"></textarea>
73
+ <select name="priority">
74
+ <option value="low">Low</option>
75
+ <option value="high">High</option>
76
+ </select>
77
+ <input type="submit" value="Submit">
78
+ </form>
79
+
80
+ <!-- Test Case 12: Inputs with only placeholder (not sufficient) -->
81
+ <div>
82
+ <input type="search" name="query" placeholder="Search...">
83
+ <input type="date" name="birthdate" placeholder="Select date">
84
+ <input type="time" name="appointment" placeholder="Select time">
85
+ </div>
86
+ </body>
87
+ </html>
package/lib/fixer.js CHANGED
@@ -3106,6 +3106,371 @@ class AccessibilityFixer {
3106
3106
  return results;
3107
3107
  }
3108
3108
 
3109
+ async fixFormLabels(directory = '.') {
3110
+ console.log(chalk.blue('📋 Fixing form labels...'));
3111
+
3112
+ const htmlFiles = await this.findHtmlFiles(directory);
3113
+ const results = [];
3114
+ let totalIssuesFound = 0;
3115
+
3116
+ for (const file of htmlFiles) {
3117
+ try {
3118
+ const content = await fs.readFile(file, 'utf8');
3119
+ const issues = this.analyzeFormLabels(content);
3120
+
3121
+ if (issues.length > 0) {
3122
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3123
+ issues.forEach(issue => {
3124
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3125
+ totalIssuesFound++;
3126
+ });
3127
+ }
3128
+
3129
+ const fixed = this.fixFormLabelsInContent(content);
3130
+
3131
+ if (fixed !== content) {
3132
+ if (this.config.backupFiles) {
3133
+ await fs.writeFile(`${file}.backup`, content);
3134
+ }
3135
+
3136
+ if (!this.config.dryRun) {
3137
+ await fs.writeFile(file, fixed);
3138
+ }
3139
+
3140
+ console.log(chalk.green(`✅ Fixed form labels in: ${file}`));
3141
+ results.push({ file, status: 'fixed', issues: issues.length });
3142
+ } else {
3143
+ results.push({ file, status: 'no-change', issues: issues.length });
3144
+ }
3145
+ } catch (error) {
3146
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
3147
+ results.push({ file, status: 'error', error: error.message });
3148
+ }
3149
+ }
3150
+
3151
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} form label issues across ${results.length} files`));
3152
+ return results;
3153
+ }
3154
+
3155
+ analyzeFormLabels(content) {
3156
+ const issues = [];
3157
+
3158
+ // Find all form input elements that need labels
3159
+ const inputElements = [
3160
+ 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
3161
+ 'input[type="tel"]', 'input[type="url"]', 'input[type="search"]',
3162
+ 'input[type="number"]', 'input[type="date"]', 'input[type="time"]',
3163
+ 'input[type="datetime-local"]', 'input[type="month"]', 'input[type="week"]',
3164
+ 'input[type="color"]', 'input[type="range"]', 'input[type="file"]',
3165
+ 'textarea', 'select'
3166
+ ];
3167
+
3168
+ // Convert to regex patterns
3169
+ const inputPatterns = [
3170
+ /<input[^>]*type\s*=\s*["'](?:text|email|password|tel|url|search|number|date|time|datetime-local|month|week|color|range|file)["'][^>]*>/gi,
3171
+ /<textarea[^>]*>/gi,
3172
+ /<select[^>]*>/gi
3173
+ ];
3174
+
3175
+ inputPatterns.forEach((pattern, patternIndex) => {
3176
+ const matches = content.match(pattern) || [];
3177
+
3178
+ matches.forEach((element, index) => {
3179
+ const elementType = patternIndex === 0 ? 'input' :
3180
+ patternIndex === 1 ? 'textarea' : 'select';
3181
+
3182
+ const issues_found = this.checkFormElementLabeling(element, content, elementType, index + 1);
3183
+ issues.push(...issues_found);
3184
+ });
3185
+ });
3186
+
3187
+ return issues;
3188
+ }
3189
+
3190
+ checkFormElementLabeling(element, content, elementType, index) {
3191
+ const issues = [];
3192
+
3193
+ // Extract element attributes
3194
+ const id = this.extractAttributeValue(element, 'id');
3195
+ const name = this.extractAttributeValue(element, 'name');
3196
+ const ariaLabel = this.extractAttributeValue(element, 'aria-label');
3197
+ const ariaLabelledby = this.extractAttributeValue(element, 'aria-labelledby');
3198
+ const title = this.extractAttributeValue(element, 'title');
3199
+ const placeholder = this.extractAttributeValue(element, 'placeholder');
3200
+
3201
+ let hasValidLabel = false;
3202
+ let labelMethods = [];
3203
+
3204
+ // Check for explicit label (label[for="id"])
3205
+ if (id) {
3206
+ const explicitLabelRegex = new RegExp(`<label[^>]*for\\s*=\\s*["']${id}["'][^>]*>([^<]+)</label>`, 'i');
3207
+ const explicitLabel = content.match(explicitLabelRegex);
3208
+ if (explicitLabel && explicitLabel[1].trim()) {
3209
+ hasValidLabel = true;
3210
+ labelMethods.push('explicit label');
3211
+ } else {
3212
+ issues.push({
3213
+ type: '📋 Missing explicit label',
3214
+ description: `${elementType} ${index} with id="${id}" does not have an explicit <label for="${id}">`,
3215
+ element: element.substring(0, 100) + '...'
3216
+ });
3217
+ }
3218
+ }
3219
+
3220
+ // Check for implicit label (wrapped in label)
3221
+ const elementPosition = content.indexOf(element);
3222
+ if (elementPosition !== -1) {
3223
+ const beforeElement = content.substring(0, elementPosition);
3224
+ const afterElement = content.substring(elementPosition + element.length);
3225
+
3226
+ // Look for wrapping label
3227
+ const labelOpenRegex = /<label[^>]*>(?:[^<]*<[^>]*>)*[^<]*$/i;
3228
+ const labelCloseRegex = /^[^<]*(?:<[^>]*>[^<]*)*<\/label>/i;
3229
+
3230
+ const hasOpenLabel = labelOpenRegex.test(beforeElement);
3231
+ const hasCloseLabel = labelCloseRegex.test(afterElement);
3232
+
3233
+ if (hasOpenLabel && hasCloseLabel) {
3234
+ // Extract label text
3235
+ const labelMatch = beforeElement.match(/<label[^>]*>([^<]*)$/i);
3236
+ const labelText = labelMatch ? labelMatch[1].trim() : '';
3237
+
3238
+ if (labelText) {
3239
+ hasValidLabel = true;
3240
+ labelMethods.push('implicit label');
3241
+ } else {
3242
+ issues.push({
3243
+ type: '📋 Empty implicit label',
3244
+ description: `${elementType} ${index} is wrapped in <label> but label text is empty`,
3245
+ element: element.substring(0, 100) + '...'
3246
+ });
3247
+ }
3248
+ } else {
3249
+ issues.push({
3250
+ type: '📋 Missing implicit label',
3251
+ description: `${elementType} ${index} does not have an implicit (wrapped) <label>`,
3252
+ element: element.substring(0, 100) + '...'
3253
+ });
3254
+ }
3255
+ }
3256
+
3257
+ // Check aria-label
3258
+ if (ariaLabel && ariaLabel.trim()) {
3259
+ hasValidLabel = true;
3260
+ labelMethods.push('aria-label');
3261
+ } else {
3262
+ issues.push({
3263
+ type: '📋 Missing aria-label',
3264
+ description: `${elementType} ${index} aria-label attribute does not exist or is empty`,
3265
+ element: element.substring(0, 100) + '...'
3266
+ });
3267
+ }
3268
+
3269
+ // Check aria-labelledby
3270
+ if (ariaLabelledby) {
3271
+ const referencedIds = ariaLabelledby.split(/\s+/);
3272
+ let validReferences = 0;
3273
+
3274
+ referencedIds.forEach(refId => {
3275
+ if (refId.trim()) {
3276
+ const referencedElement = content.match(new RegExp(`<[^>]*id\\s*=\\s*["']${refId}["'][^>]*>([^<]*)</[^>]*>`, 'i'));
3277
+ if (referencedElement && referencedElement[1].trim()) {
3278
+ validReferences++;
3279
+ }
3280
+ }
3281
+ });
3282
+
3283
+ if (validReferences > 0) {
3284
+ hasValidLabel = true;
3285
+ labelMethods.push('aria-labelledby');
3286
+ } else {
3287
+ issues.push({
3288
+ type: '📋 Invalid aria-labelledby',
3289
+ description: `${elementType} ${index} aria-labelledby references elements that do not exist or are empty`,
3290
+ element: element.substring(0, 100) + '...'
3291
+ });
3292
+ }
3293
+ } else {
3294
+ issues.push({
3295
+ type: '📋 Missing aria-labelledby',
3296
+ description: `${elementType} ${index} aria-labelledby attribute does not exist`,
3297
+ element: element.substring(0, 100) + '...'
3298
+ });
3299
+ }
3300
+
3301
+ // Check title attribute
3302
+ if (!title || !title.trim()) {
3303
+ issues.push({
3304
+ type: '📋 Missing title',
3305
+ description: `${elementType} ${index} has no title attribute`,
3306
+ element: element.substring(0, 100) + '...'
3307
+ });
3308
+ }
3309
+
3310
+ // Check if element needs role="none" or role="presentation" to override default semantics
3311
+ const hasRole = /role\s*=/i.test(element);
3312
+ if (!hasRole && !hasValidLabel) {
3313
+ issues.push({
3314
+ type: '📋 Missing role override',
3315
+ description: `${elementType} ${index} default semantics were not overridden with role="none" or role="presentation"`,
3316
+ element: element.substring(0, 100) + '...'
3317
+ });
3318
+ }
3319
+
3320
+ return issues;
3321
+ }
3322
+
3323
+ fixFormLabelsInContent(content) {
3324
+ let fixed = content;
3325
+
3326
+ // Fix input elements
3327
+ const inputPatterns = [
3328
+ /<input[^>]*type\s*=\s*["'](?:text|email|password|tel|url|search|number|date|time|datetime-local|month|week|color|range|file)["'][^>]*>/gi,
3329
+ /<textarea[^>]*>/gi,
3330
+ /<select[^>]*>/gi
3331
+ ];
3332
+
3333
+ inputPatterns.forEach(pattern => {
3334
+ fixed = fixed.replace(pattern, (match) => {
3335
+ return this.addFormElementLabeling(match, fixed);
3336
+ });
3337
+ });
3338
+
3339
+ return fixed;
3340
+ }
3341
+
3342
+ addFormElementLabeling(element, content) {
3343
+ let enhanced = element;
3344
+
3345
+ // Extract current attributes
3346
+ const id = this.extractAttributeValue(element, 'id');
3347
+ const name = this.extractAttributeValue(element, 'name');
3348
+ const ariaLabel = this.extractAttributeValue(element, 'aria-label');
3349
+ const title = this.extractAttributeValue(element, 'title');
3350
+ const placeholder = this.extractAttributeValue(element, 'placeholder');
3351
+
3352
+ // Generate appropriate label text
3353
+ let labelText = this.generateFormLabelText(element, name, placeholder);
3354
+
3355
+ // Add aria-label if missing
3356
+ if (!ariaLabel && labelText) {
3357
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 aria-label="${labelText}"$2`);
3358
+ console.log(chalk.yellow(` 📋 Added aria-label="${labelText}" to form element`));
3359
+ }
3360
+
3361
+ // Add title if missing
3362
+ if (!title && labelText) {
3363
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 title="${labelText}"$2`);
3364
+ console.log(chalk.yellow(` 📋 Added title="${labelText}" to form element`));
3365
+ }
3366
+
3367
+ // Add id if missing (for potential explicit labeling)
3368
+ if (!id) {
3369
+ const generatedId = this.generateFormElementId(element, name);
3370
+ enhanced = enhanced.replace(/(<(?:input|textarea|select)[^>]*?)(\s*\/?>)/i, `$1 id="${generatedId}"$2`);
3371
+ console.log(chalk.yellow(` 📋 Added id="${generatedId}" to form element`));
3372
+ }
3373
+
3374
+ return enhanced;
3375
+ }
3376
+
3377
+ generateFormLabelText(element, name, placeholder) {
3378
+ const lang = this.config.language;
3379
+
3380
+ // Try to extract meaningful text from various sources
3381
+ if (placeholder && placeholder.trim()) {
3382
+ return placeholder.trim();
3383
+ }
3384
+
3385
+ if (name && name.trim()) {
3386
+ // Convert name to readable text
3387
+ const readable = name.replace(/[-_]/g, ' ')
3388
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
3389
+ .toLowerCase();
3390
+
3391
+ // Capitalize first letter
3392
+ return readable.charAt(0).toUpperCase() + readable.slice(1);
3393
+ }
3394
+
3395
+ // Extract input type for generic labels
3396
+ const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
3397
+ const inputType = typeMatch ? typeMatch[1] : 'text';
3398
+
3399
+ // Generate type-specific labels
3400
+ const typeLabels = {
3401
+ ja: {
3402
+ text: 'テキスト入力',
3403
+ email: 'メールアドレス',
3404
+ password: 'パスワード',
3405
+ tel: '電話番号',
3406
+ url: 'URL',
3407
+ search: '検索',
3408
+ number: '数値',
3409
+ date: '日付',
3410
+ time: '時刻',
3411
+ file: 'ファイル選択',
3412
+ textarea: 'テキストエリア',
3413
+ select: '選択'
3414
+ },
3415
+ en: {
3416
+ text: 'Text input',
3417
+ email: 'Email address',
3418
+ password: 'Password',
3419
+ tel: 'Phone number',
3420
+ url: 'URL',
3421
+ search: 'Search',
3422
+ number: 'Number',
3423
+ date: 'Date',
3424
+ time: 'Time',
3425
+ file: 'File selection',
3426
+ textarea: 'Text area',
3427
+ select: 'Selection'
3428
+ },
3429
+ vi: {
3430
+ text: 'Nhập văn bản',
3431
+ email: 'Địa chỉ email',
3432
+ password: 'Mật khẩu',
3433
+ tel: 'Số điện thoại',
3434
+ url: 'URL',
3435
+ search: 'Tìm kiếm',
3436
+ number: 'Số',
3437
+ date: 'Ngày',
3438
+ time: 'Thời gian',
3439
+ file: 'Chọn file',
3440
+ textarea: 'Vùng văn bản',
3441
+ select: 'Lựa chọn'
3442
+ }
3443
+ };
3444
+
3445
+ const labels = typeLabels[lang] || typeLabels.en;
3446
+
3447
+ // Determine element type
3448
+ let elementType = inputType;
3449
+ if (element.includes('<textarea')) elementType = 'textarea';
3450
+ if (element.includes('<select')) elementType = 'select';
3451
+
3452
+ return labels[elementType] || labels.text;
3453
+ }
3454
+
3455
+ generateFormElementId(element, name) {
3456
+ if (name) {
3457
+ return `${name}_input`;
3458
+ }
3459
+
3460
+ // Generate based on type
3461
+ const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
3462
+ const inputType = typeMatch ? typeMatch[1] : 'text';
3463
+
3464
+ const timestamp = Date.now().toString().slice(-6);
3465
+ return `${inputType}_${timestamp}`;
3466
+ }
3467
+
3468
+ extractAttributeValue(element, attributeName) {
3469
+ const regex = new RegExp(`${attributeName}\\s*=\\s*["']([^"']*)["']`, 'i');
3470
+ const match = element.match(regex);
3471
+ return match ? match[1] : null;
3472
+ }
3473
+
3109
3474
  analyzeHeadingStructure(content) {
3110
3475
  const issues = [];
3111
3476
 
@@ -3177,6 +3542,557 @@ class AccessibilityFixer {
3177
3542
  return issues;
3178
3543
  }
3179
3544
 
3545
+ async fixAllAccessibilityIssues(directory = '.') {
3546
+ console.log(chalk.blue('🚀 Starting comprehensive accessibility fixes...'));
3547
+ console.log('');
3548
+
3549
+ const results = {
3550
+ totalFiles: 0,
3551
+ fixedFiles: 0,
3552
+ totalIssues: 0,
3553
+ steps: []
3554
+ };
3555
+
3556
+ try {
3557
+ // Step 1: HTML lang attributes
3558
+ console.log(chalk.blue('📝 Step 1: HTML lang attributes...'));
3559
+ const langResults = await this.fixHtmlLang(directory);
3560
+ const langFixed = langResults.filter(r => r.status === 'fixed').length;
3561
+ results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
3562
+
3563
+ // Step 2: Alt attributes
3564
+ console.log(chalk.blue('🖼️ Step 2: Alt attributes...'));
3565
+ const altResults = await this.fixEmptyAltAttributes(directory);
3566
+ const altFixed = altResults.filter(r => r.status === 'fixed').length;
3567
+ const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3568
+ results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
3569
+
3570
+ // Step 3: Role attributes
3571
+ console.log(chalk.blue('🎭 Step 3: Role attributes...'));
3572
+ const roleResults = await this.fixRoleAttributes(directory);
3573
+ const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
3574
+ const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3575
+ results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
3576
+
3577
+ // Step 4: Form labels
3578
+ console.log(chalk.blue('📋 Step 4: Form labels...'));
3579
+ const formResults = await this.fixFormLabels(directory);
3580
+ const formFixed = formResults.filter(r => r.status === 'fixed').length;
3581
+ const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3582
+ results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
3583
+
3584
+ // Step 5: Button names
3585
+ console.log(chalk.blue('🔘 Step 5: Button names...'));
3586
+ const buttonResults = await this.fixButtonNames(directory);
3587
+ const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
3588
+ const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3589
+ results.steps.push({ step: 5, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
3590
+
3591
+ // Step 6: Link names
3592
+ console.log(chalk.blue('🔗 Step 6: Link names...'));
3593
+ const linkResults = await this.fixLinkNames(directory);
3594
+ const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
3595
+ const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3596
+ results.steps.push({ step: 6, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
3597
+
3598
+ // Step 7: Landmarks
3599
+ console.log(chalk.blue('🏛️ Step 7: Landmarks...'));
3600
+ const landmarkResults = await this.fixLandmarks(directory);
3601
+ const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
3602
+ const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3603
+ results.steps.push({ step: 7, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
3604
+
3605
+ // Step 8: Heading analysis
3606
+ console.log(chalk.blue('📑 Step 8: Heading analysis...'));
3607
+ const headingResults = await this.analyzeHeadings(directory);
3608
+ const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3609
+ results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
3610
+ console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
3611
+
3612
+ // Step 9: Broken links check
3613
+ console.log(chalk.blue('🔗 Step 9: Broken links check...'));
3614
+ const brokenLinksResults = await this.checkBrokenLinks(directory);
3615
+ const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
3616
+ results.steps.push({ step: 9, name: 'Broken links check', issues: totalBrokenLinks });
3617
+ console.log(chalk.gray('💡 Broken link issues require manual review and cannot be auto-fixed'));
3618
+
3619
+ // Step 10: Cleanup duplicate roles
3620
+ console.log(chalk.blue('🧹 Step 10: Cleanup duplicate roles...'));
3621
+ const cleanupResults = await this.cleanupDuplicateRoles(directory);
3622
+ const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
3623
+ results.steps.push({ step: 10, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
3624
+
3625
+ // Calculate totals
3626
+ results.totalFiles = Math.max(
3627
+ langResults.length, altResults.length, roleResults.length, formResults.length,
3628
+ buttonResults.length, linkResults.length, landmarkResults.length,
3629
+ headingResults.length, brokenLinksResults.length, cleanupResults.length
3630
+ );
3631
+
3632
+ results.fixedFiles = new Set([
3633
+ ...langResults.filter(r => r.status === 'fixed').map(r => r.file),
3634
+ ...altResults.filter(r => r.status === 'fixed').map(r => r.file),
3635
+ ...roleResults.filter(r => r.status === 'fixed').map(r => r.file),
3636
+ ...formResults.filter(r => r.status === 'fixed').map(r => r.file),
3637
+ ...buttonResults.filter(r => r.status === 'fixed').map(r => r.file),
3638
+ ...linkResults.filter(r => r.status === 'fixed').map(r => r.file),
3639
+ ...landmarkResults.filter(r => r.status === 'fixed').map(r => r.file),
3640
+ ...cleanupResults.filter(r => r.status === 'fixed').map(r => r.file)
3641
+ ]).size;
3642
+
3643
+ results.totalIssues = totalAltIssues + totalRoleIssues + totalFormIssues +
3644
+ totalButtonIssues + totalLinkIssues + totalLandmarkIssues;
3645
+
3646
+ // Final summary
3647
+ console.log(chalk.green('\n🎉 All accessibility fixes completed!'));
3648
+ console.log(chalk.blue('📊 Final Summary:'));
3649
+ console.log(chalk.blue(` Total files scanned: ${results.totalFiles}`));
3650
+ console.log(chalk.blue(` Files fixed: ${results.fixedFiles}`));
3651
+ console.log(chalk.blue(` Total issues resolved: ${results.totalIssues}`));
3652
+
3653
+ if (this.config.dryRun) {
3654
+ console.log(chalk.yellow('\n💡 This was a dry run. Use without --dry-run to apply changes.'));
3655
+ }
3656
+
3657
+ return results;
3658
+
3659
+ } catch (error) {
3660
+ console.error(chalk.red(`❌ Error during comprehensive fixes: ${error.message}`));
3661
+ throw error;
3662
+ }
3663
+ }
3664
+
3665
+ async fixButtonNames(directory = '.') {
3666
+ console.log(chalk.blue('🔘 Fixing button names...'));
3667
+
3668
+ const htmlFiles = await this.findHtmlFiles(directory);
3669
+ const results = [];
3670
+ let totalIssuesFound = 0;
3671
+
3672
+ for (const file of htmlFiles) {
3673
+ try {
3674
+ const content = await fs.readFile(file, 'utf8');
3675
+ const issues = this.analyzeButtonNames(content);
3676
+
3677
+ if (issues.length > 0) {
3678
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3679
+ issues.forEach(issue => {
3680
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3681
+ totalIssuesFound++;
3682
+ });
3683
+ }
3684
+
3685
+ const fixed = this.fixButtonNamesInContent(content);
3686
+
3687
+ if (fixed !== content) {
3688
+ if (this.config.backupFiles) {
3689
+ await fs.writeFile(`${file}.backup`, content);
3690
+ }
3691
+
3692
+ if (!this.config.dryRun) {
3693
+ await fs.writeFile(file, fixed);
3694
+ }
3695
+
3696
+ console.log(chalk.green(`✅ Fixed button names in: ${file}`));
3697
+ results.push({ file, status: 'fixed', issues: issues.length });
3698
+ } else {
3699
+ results.push({ file, status: 'no-change', issues: issues.length });
3700
+ }
3701
+ } catch (error) {
3702
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
3703
+ results.push({ file, status: 'error', error: error.message });
3704
+ }
3705
+ }
3706
+
3707
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} button name issues across ${results.length} files`));
3708
+ return results;
3709
+ }
3710
+
3711
+ analyzeButtonNames(content) {
3712
+ const issues = [];
3713
+ const buttonPattern = /<button[^>]*>[\s\S]*?<\/button>/gi;
3714
+ const buttons = content.match(buttonPattern) || [];
3715
+
3716
+ buttons.forEach((button, index) => {
3717
+ const buttonText = button.replace(/<[^>]*>/g, '').trim();
3718
+ const hasAriaLabel = /aria-label\s*=/i.test(button);
3719
+ const hasTitle = /title\s*=/i.test(button);
3720
+
3721
+ if (!buttonText && !hasAriaLabel && !hasTitle) {
3722
+ issues.push({
3723
+ type: '🔘 Empty button',
3724
+ description: `Button ${index + 1} has no text content, aria-label, or title`,
3725
+ element: button.substring(0, 100) + '...'
3726
+ });
3727
+ }
3728
+ });
3729
+
3730
+ return issues;
3731
+ }
3732
+
3733
+ fixButtonNamesInContent(content) {
3734
+ let fixed = content;
3735
+
3736
+ const buttonPattern = /<button([^>]*)>([\s\S]*?)<\/button>/gi;
3737
+
3738
+ fixed = fixed.replace(buttonPattern, (match, attributes, innerContent) => {
3739
+ const buttonText = innerContent.replace(/<[^>]*>/g, '').trim();
3740
+ const hasAriaLabel = /aria-label\s*=/i.test(attributes);
3741
+ const hasTitle = /title\s*=/i.test(attributes);
3742
+
3743
+ if (!buttonText && !hasAriaLabel && !hasTitle) {
3744
+ const buttonName = this.generateButtonName(attributes, innerContent);
3745
+ const updatedAttributes = attributes + ` aria-label="${buttonName}" title="${buttonName}"`;
3746
+ console.log(chalk.yellow(` 🔘 Added aria-label and title to empty button: "${buttonName}"`));
3747
+ return `<button${updatedAttributes}>${innerContent}</button>`;
3748
+ }
3749
+
3750
+ return match;
3751
+ });
3752
+
3753
+ return fixed;
3754
+ }
3755
+
3756
+ generateButtonName(attributes, innerContent) {
3757
+ const lang = this.config.language;
3758
+
3759
+ // Try to extract meaningful name from onclick or other attributes
3760
+ const onclickMatch = attributes.match(/onclick\s*=\s*["']([^"']+)["']/i);
3761
+ if (onclickMatch) {
3762
+ const onclick = onclickMatch[1];
3763
+ if (onclick.includes('submit')) return lang === 'ja' ? '送信' : 'Submit';
3764
+ if (onclick.includes('cancel')) return lang === 'ja' ? 'キャンセル' : 'Cancel';
3765
+ if (onclick.includes('close')) return lang === 'ja' ? '閉じる' : 'Close';
3766
+ if (onclick.includes('save')) return lang === 'ja' ? '保存' : 'Save';
3767
+ }
3768
+
3769
+ // Check for common class names
3770
+ const classMatch = attributes.match(/class\s*=\s*["']([^"']+)["']/i);
3771
+ if (classMatch) {
3772
+ const className = classMatch[1].toLowerCase();
3773
+ if (className.includes('submit')) return lang === 'ja' ? '送信' : 'Submit';
3774
+ if (className.includes('cancel')) return lang === 'ja' ? 'キャンセル' : 'Cancel';
3775
+ if (className.includes('close')) return lang === 'ja' ? '閉じる' : 'Close';
3776
+ if (className.includes('save')) return lang === 'ja' ? '保存' : 'Save';
3777
+ }
3778
+
3779
+ return lang === 'ja' ? 'ボタン' : 'Button';
3780
+ }
3781
+
3782
+ async fixLinkNames(directory = '.') {
3783
+ console.log(chalk.blue('🔗 Fixing link names...'));
3784
+
3785
+ const htmlFiles = await this.findHtmlFiles(directory);
3786
+ const results = [];
3787
+ let totalIssuesFound = 0;
3788
+
3789
+ for (const file of htmlFiles) {
3790
+ try {
3791
+ const content = await fs.readFile(file, 'utf8');
3792
+ const issues = this.analyzeLinkNames(content);
3793
+
3794
+ if (issues.length > 0) {
3795
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3796
+ issues.forEach(issue => {
3797
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3798
+ totalIssuesFound++;
3799
+ });
3800
+ }
3801
+
3802
+ const fixed = this.fixLinkNamesInContent(content);
3803
+
3804
+ if (fixed !== content) {
3805
+ if (this.config.backupFiles) {
3806
+ await fs.writeFile(`${file}.backup`, content);
3807
+ }
3808
+
3809
+ if (!this.config.dryRun) {
3810
+ await fs.writeFile(file, fixed);
3811
+ }
3812
+
3813
+ console.log(chalk.green(`✅ Fixed link names in: ${file}`));
3814
+ results.push({ file, status: 'fixed', issues: issues.length });
3815
+ } else {
3816
+ results.push({ file, status: 'no-change', issues: issues.length });
3817
+ }
3818
+ } catch (error) {
3819
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
3820
+ results.push({ file, status: 'error', error: error.message });
3821
+ }
3822
+ }
3823
+
3824
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} link name issues across ${results.length} files`));
3825
+ return results;
3826
+ }
3827
+
3828
+ analyzeLinkNames(content) {
3829
+ const issues = [];
3830
+ const linkPattern = /<a[^>]*href[^>]*>[\s\S]*?<\/a>/gi;
3831
+ const links = content.match(linkPattern) || [];
3832
+
3833
+ links.forEach((link, index) => {
3834
+ const linkText = link.replace(/<[^>]*>/g, '').trim();
3835
+ const hasAriaLabel = /aria-label\s*=/i.test(link);
3836
+ const hasTitle = /title\s*=/i.test(link);
3837
+
3838
+ if (!linkText && !hasAriaLabel && !hasTitle) {
3839
+ issues.push({
3840
+ type: '🔗 Empty link',
3841
+ description: `Link ${index + 1} has no text content, aria-label, or title`,
3842
+ element: link.substring(0, 100) + '...'
3843
+ });
3844
+ }
3845
+
3846
+ // Check for generic link text
3847
+ const genericTexts = ['click here', 'read more', 'more', 'here', 'link'];
3848
+ if (genericTexts.some(generic => linkText.toLowerCase().includes(generic))) {
3849
+ issues.push({
3850
+ type: '🔗 Generic link text',
3851
+ description: `Link ${index + 1} has generic text: "${linkText}"`,
3852
+ element: link.substring(0, 100) + '...'
3853
+ });
3854
+ }
3855
+ });
3856
+
3857
+ return issues;
3858
+ }
3859
+
3860
+ fixLinkNamesInContent(content) {
3861
+ let fixed = content;
3862
+
3863
+ const linkPattern = /<a([^>]*href[^>]*?)>([\s\S]*?)<\/a>/gi;
3864
+
3865
+ fixed = fixed.replace(linkPattern, (match, attributes, innerContent) => {
3866
+ const linkText = innerContent.replace(/<[^>]*>/g, '').trim();
3867
+ const hasAriaLabel = /aria-label\s*=/i.test(attributes);
3868
+ const hasTitle = /title\s*=/i.test(attributes);
3869
+
3870
+ if (!linkText && !hasAriaLabel && !hasTitle) {
3871
+ const linkName = this.generateLinkName(attributes, innerContent);
3872
+ const updatedAttributes = attributes + ` aria-label="${linkName}" title="${linkName}"`;
3873
+ console.log(chalk.yellow(` 🔗 Added aria-label and title to empty link: "${linkName}"`));
3874
+ return `<a${updatedAttributes}>${innerContent}</a>`;
3875
+ }
3876
+
3877
+ return match;
3878
+ });
3879
+
3880
+ return fixed;
3881
+ }
3882
+
3883
+ generateLinkName(attributes, innerContent) {
3884
+ const lang = this.config.language;
3885
+
3886
+ // Try to extract href for context
3887
+ const hrefMatch = attributes.match(/href\s*=\s*["']([^"']+)["']/i);
3888
+ if (hrefMatch) {
3889
+ const href = hrefMatch[1];
3890
+ if (href.includes('mailto:')) return lang === 'ja' ? 'メール送信' : 'Send email';
3891
+ if (href.includes('tel:')) return lang === 'ja' ? '電話をかける' : 'Make call';
3892
+ if (href.includes('#')) return lang === 'ja' ? 'ページ内リンク' : 'Page anchor';
3893
+ if (href.includes('.pdf')) return lang === 'ja' ? 'PDFを開く' : 'Open PDF';
3894
+ }
3895
+
3896
+ return lang === 'ja' ? 'リンク' : 'Link';
3897
+ }
3898
+
3899
+ async fixLandmarks(directory = '.') {
3900
+ console.log(chalk.blue('🏛️ Fixing landmarks...'));
3901
+
3902
+ const htmlFiles = await this.findHtmlFiles(directory);
3903
+ const results = [];
3904
+ let totalIssuesFound = 0;
3905
+
3906
+ for (const file of htmlFiles) {
3907
+ try {
3908
+ const content = await fs.readFile(file, 'utf8');
3909
+ const issues = this.analyzeLandmarks(content);
3910
+
3911
+ if (issues.length > 0) {
3912
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3913
+ issues.forEach(issue => {
3914
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3915
+ totalIssuesFound++;
3916
+ });
3917
+ }
3918
+
3919
+ results.push({ file, status: 'no-change', issues: issues.length });
3920
+ } catch (error) {
3921
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
3922
+ results.push({ file, status: 'error', error: error.message });
3923
+ }
3924
+ }
3925
+
3926
+ console.log(chalk.blue(`\n📊 Summary: Found ${totalIssuesFound} landmark issues across ${results.length} files`));
3927
+ return results;
3928
+ }
3929
+
3930
+ analyzeLandmarks(content) {
3931
+ const issues = [];
3932
+
3933
+ const hasMain = /<main[^>]*>/i.test(content);
3934
+ const hasNav = /<nav[^>]*>/i.test(content);
3935
+ const hasHeader = /<header[^>]*>/i.test(content);
3936
+ const hasFooter = /<footer[^>]*>/i.test(content);
3937
+
3938
+ if (!hasMain) {
3939
+ issues.push({
3940
+ type: '🏛️ Missing main landmark',
3941
+ description: 'Page should have a main landmark',
3942
+ suggestion: 'Add <main> element around primary content'
3943
+ });
3944
+ }
3945
+
3946
+ return issues;
3947
+ }
3948
+
3949
+ async analyzeHeadings(directory = '.') {
3950
+ console.log(chalk.blue('📑 Analyzing heading structure...'));
3951
+
3952
+ const htmlFiles = await this.findHtmlFiles(directory);
3953
+ const results = [];
3954
+ let totalIssuesFound = 0;
3955
+
3956
+ for (const file of htmlFiles) {
3957
+ try {
3958
+ const content = await fs.readFile(file, 'utf8');
3959
+ const issues = this.analyzeHeadingStructure(content);
3960
+
3961
+ if (issues.length > 0) {
3962
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3963
+ issues.forEach(issue => {
3964
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
3965
+ if (issue.suggestion) {
3966
+ console.log(chalk.gray(` 💡 ${issue.suggestion}`));
3967
+ }
3968
+ totalIssuesFound++;
3969
+ });
3970
+ }
3971
+
3972
+ results.push({ file, status: 'analyzed', issues: issues.length });
3973
+ } catch (error) {
3974
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
3975
+ results.push({ file, status: 'error', error: error.message });
3976
+ }
3977
+ }
3978
+
3979
+ console.log(chalk.blue(`\n📊 Summary: Analyzed heading structure in ${results.length} files`));
3980
+ console.log(chalk.gray('💡 Heading issues require manual review and cannot be auto-fixed'));
3981
+ return results;
3982
+ }
3983
+
3984
+ async checkBrokenLinks(directory = '.') {
3985
+ console.log(chalk.blue('🔗 Checking for broken links and 404 resources...'));
3986
+
3987
+ const htmlFiles = await this.findHtmlFiles(directory);
3988
+ const results = [];
3989
+ let totalIssuesFound = 0;
3990
+
3991
+ for (const file of htmlFiles) {
3992
+ try {
3993
+ const content = await fs.readFile(file, 'utf8');
3994
+ const issues = this.analyzeBrokenLinks(content, file);
3995
+
3996
+ if (issues.length > 0) {
3997
+ console.log(chalk.cyan(`\n📁 ${file}:`));
3998
+ issues.forEach(issue => {
3999
+ console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
4000
+ if (issue.suggestion) {
4001
+ console.log(chalk.gray(` 💡 ${issue.suggestion}`));
4002
+ }
4003
+ totalIssuesFound++;
4004
+ });
4005
+ }
4006
+
4007
+ results.push({ file, status: 'analyzed', issues: issues.length });
4008
+ } catch (error) {
4009
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
4010
+ results.push({ file, status: 'error', error: error.message });
4011
+ }
4012
+ }
4013
+
4014
+ console.log(chalk.blue(`\n📊 Summary: Analyzed links in ${results.length} files`));
4015
+ console.log(chalk.gray('💡 Broken link issues require manual review and cannot be auto-fixed'));
4016
+ return results;
4017
+ }
4018
+
4019
+ analyzeBrokenLinks(content, filePath) {
4020
+ const issues = [];
4021
+
4022
+ // Check for local image files
4023
+ const imgPattern = /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
4024
+ let match;
4025
+
4026
+ while ((match = imgPattern.exec(content)) !== null) {
4027
+ const src = match[1];
4028
+
4029
+ // Skip external URLs and data URLs
4030
+ if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('//')) {
4031
+ continue;
4032
+ }
4033
+
4034
+ // Check if local file exists
4035
+ const fullPath = path.resolve(path.dirname(filePath), src);
4036
+ try {
4037
+ require('fs').statSync(fullPath);
4038
+ } catch (error) {
4039
+ issues.push({
4040
+ type: '📁 Image not found',
4041
+ description: `img file does not exist: ${src}`,
4042
+ suggestion: 'Create the missing file or update the image path'
4043
+ });
4044
+ }
4045
+ }
4046
+
4047
+ return issues;
4048
+ }
4049
+
4050
+ async cleanupDuplicateRoles(directory = '.') {
4051
+ console.log(chalk.blue('🧹 Cleaning up duplicate role attributes...'));
4052
+
4053
+ const htmlFiles = await this.findHtmlFiles(directory);
4054
+ const results = [];
4055
+ let totalIssuesFound = 0;
4056
+
4057
+ for (const file of htmlFiles) {
4058
+ try {
4059
+ const content = await fs.readFile(file, 'utf8');
4060
+ const fixed = this.cleanupDuplicateRolesInContent(content);
4061
+
4062
+ if (fixed !== content) {
4063
+ if (this.config.backupFiles) {
4064
+ await fs.writeFile(`${file}.backup`, content);
4065
+ }
4066
+
4067
+ if (!this.config.dryRun) {
4068
+ await fs.writeFile(file, fixed);
4069
+ }
4070
+
4071
+ console.log(chalk.green(`✅ Cleaned duplicate roles in: ${file}`));
4072
+ results.push({ file, status: 'fixed' });
4073
+ totalIssuesFound++;
4074
+ } else {
4075
+ results.push({ file, status: 'no-change' });
4076
+ }
4077
+ } catch (error) {
4078
+ console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
4079
+ results.push({ file, status: 'error', error: error.message });
4080
+ }
4081
+ }
4082
+
4083
+ console.log(chalk.blue(`\n📊 Summary: Cleaned duplicate roles in ${totalIssuesFound} files`));
4084
+ return results;
4085
+ }
4086
+
4087
+ cleanupDuplicateRolesInContent(content) {
4088
+ let fixed = content;
4089
+
4090
+ // Remove duplicate role attributes
4091
+ fixed = fixed.replace(/(\s+role\s*=\s*["'][^"']*["'])\s+role\s*=\s*["'][^"']*["']/gi, '$1');
4092
+
4093
+ return fixed;
4094
+ }
4095
+
3180
4096
  async findHtmlFiles(directory) {
3181
4097
  const files = [];
3182
4098
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbu-accessibility-package",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Comprehensive accessibility fixes for HTML files. Smart context-aware alt text generation, form labels, button names, link names, landmarks, heading analysis, and WCAG-compliant role attributes. Covers major axe DevTools issues with individual fix modes.",
5
5
  "main": "index.js",
6
6
  "bin": {