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 +38 -0
- package/README-vi.md +85 -0
- package/README.md +82 -0
- package/demo/form-labels-test.html +87 -0
- package/lib/fixer.js +916 -0
- package/package.json +1 -1
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.
|
|
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": {
|