gbu-accessibility-package 3.2.0 → 3.2.1
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/CHANGELOG.md +20 -0
- package/README-vi.md +45 -28
- package/README.md +21 -15
- package/cli.js +21 -1
- package/demo/broken-links-test.html +41 -0
- package/index.js +1 -2
- package/lib/fixer.js +1436 -5
- package/package.json +5 -4
- package/lib/enhanced-alt-checker.js +0 -632
- package/lib/enhanced-alt-generator.js +0 -741
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbu-accessibility-package",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
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": {
|
|
7
|
-
"gbu-a11y": "
|
|
8
|
-
"accessibility-fixer": "
|
|
7
|
+
"gbu-a11y": "cli.js",
|
|
8
|
+
"accessibility-fixer": "cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node cli.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"links-only": "node cli.js --links-only",
|
|
21
21
|
"landmarks-only": "node cli.js --landmarks-only",
|
|
22
22
|
"headings-only": "node cli.js --headings-only",
|
|
23
|
+
"links-check": "node cli.js --links-check",
|
|
23
24
|
"cleanup-only": "node cli.js --cleanup-only",
|
|
24
25
|
"no-backup": "node cli.js --no-backup",
|
|
25
26
|
"cleanup-backups": "find . -name '*.backup' -type f -delete",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"license": "MIT",
|
|
61
62
|
"repository": {
|
|
62
63
|
"type": "git",
|
|
63
|
-
"url": "https://github.com/dangpv94/gbu-accessibility-tool.git"
|
|
64
|
+
"url": "git+https://github.com/dangpv94/gbu-accessibility-tool.git"
|
|
64
65
|
},
|
|
65
66
|
"homepage": "https://github.com/dangpv94/gbu-accessibility-tool#readme",
|
|
66
67
|
"bugs": {
|
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enhanced Alt Attribute Checker
|
|
3
|
-
* Cải tiến tính năng kiểm tra alt attribute đa dạng và toàn diện hơn
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const chalk = require('chalk');
|
|
7
|
-
|
|
8
|
-
class EnhancedAltChecker {
|
|
9
|
-
constructor(config = {}) {
|
|
10
|
-
this.config = {
|
|
11
|
-
language: config.language || 'ja',
|
|
12
|
-
strictMode: config.strictMode || false,
|
|
13
|
-
checkDecorative: config.checkDecorative || true,
|
|
14
|
-
checkInformative: config.checkInformative || true,
|
|
15
|
-
checkComplex: config.checkComplex || true,
|
|
16
|
-
maxAltLength: config.maxAltLength || 125,
|
|
17
|
-
minAltLength: config.minAltLength || 3,
|
|
18
|
-
...config
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Phân tích toàn diện các vấn đề alt attribute
|
|
24
|
-
*/
|
|
25
|
-
analyzeAltAttributes(content) {
|
|
26
|
-
const issues = [];
|
|
27
|
-
const imgRegex = /<img[^>]*>/gi;
|
|
28
|
-
const imgTags = content.match(imgRegex) || [];
|
|
29
|
-
|
|
30
|
-
imgTags.forEach((imgTag, index) => {
|
|
31
|
-
const analysis = this.analyzeImageContext(imgTag, content, index);
|
|
32
|
-
const altIssues = this.checkAltQuality(imgTag, analysis);
|
|
33
|
-
|
|
34
|
-
if (altIssues.length > 0) {
|
|
35
|
-
issues.push({
|
|
36
|
-
imageIndex: index + 1,
|
|
37
|
-
imgTag: imgTag,
|
|
38
|
-
src: analysis.src,
|
|
39
|
-
context: analysis.context,
|
|
40
|
-
issues: altIssues,
|
|
41
|
-
recommendations: this.generateRecommendations(imgTag, analysis)
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return issues;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Phân tích ngữ cảnh và mục đích của hình ảnh
|
|
51
|
-
*/
|
|
52
|
-
analyzeImageContext(imgTag, htmlContent, imgIndex) {
|
|
53
|
-
const src = this.extractAttribute(imgTag, 'src');
|
|
54
|
-
const alt = this.extractAttribute(imgTag, 'alt');
|
|
55
|
-
const title = this.extractAttribute(imgTag, 'title');
|
|
56
|
-
const ariaLabel = this.extractAttribute(imgTag, 'aria-label');
|
|
57
|
-
const role = this.extractAttribute(imgTag, 'role');
|
|
58
|
-
|
|
59
|
-
// Phân tích vị trí và ngữ cảnh
|
|
60
|
-
const position = this.findImagePosition(imgTag, htmlContent, imgIndex);
|
|
61
|
-
const surroundingContext = this.extractSurroundingContext(htmlContent, position, 1000);
|
|
62
|
-
|
|
63
|
-
// Xác định loại hình ảnh
|
|
64
|
-
const imageType = this.classifyImageType(imgTag, surroundingContext, src);
|
|
65
|
-
|
|
66
|
-
// Phân tích cấu trúc HTML xung quanh
|
|
67
|
-
const structuralContext = this.analyzeStructuralContext(surroundingContext, imgTag);
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
src,
|
|
71
|
-
alt,
|
|
72
|
-
title,
|
|
73
|
-
ariaLabel,
|
|
74
|
-
role,
|
|
75
|
-
imageType,
|
|
76
|
-
context: surroundingContext,
|
|
77
|
-
structural: structuralContext,
|
|
78
|
-
position
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Kiểm tra chất lượng alt text theo nhiều tiêu chí
|
|
84
|
-
*/
|
|
85
|
-
checkAltQuality(imgTag, analysis) {
|
|
86
|
-
const issues = [];
|
|
87
|
-
const { alt, imageType, src } = analysis;
|
|
88
|
-
|
|
89
|
-
// 1. Kiểm tra cơ bản - thiếu alt
|
|
90
|
-
if (!this.hasAttribute(imgTag, 'alt')) {
|
|
91
|
-
issues.push({
|
|
92
|
-
type: 'MISSING_ALT',
|
|
93
|
-
severity: 'ERROR',
|
|
94
|
-
message: 'Thiếu thuộc tính alt',
|
|
95
|
-
description: 'Tất cả hình ảnh phải có thuộc tính alt'
|
|
96
|
-
});
|
|
97
|
-
return issues; // Không cần kiểm tra thêm nếu thiếu alt
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 2. Kiểm tra alt rỗng
|
|
101
|
-
if (alt === '') {
|
|
102
|
-
if (imageType === 'decorative') {
|
|
103
|
-
// OK cho hình trang trí
|
|
104
|
-
return issues;
|
|
105
|
-
} else {
|
|
106
|
-
issues.push({
|
|
107
|
-
type: 'EMPTY_ALT',
|
|
108
|
-
severity: 'ERROR',
|
|
109
|
-
message: 'Alt text rỗng cho hình ảnh có nội dung',
|
|
110
|
-
description: 'Hình ảnh có nội dung cần alt text mô tả'
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 3. Kiểm tra độ dài alt text
|
|
116
|
-
if (alt && alt.length > this.config.maxAltLength) {
|
|
117
|
-
issues.push({
|
|
118
|
-
type: 'ALT_TOO_LONG',
|
|
119
|
-
severity: 'WARNING',
|
|
120
|
-
message: `Alt text quá dài (${alt.length} ký tự)`,
|
|
121
|
-
description: `Nên giới hạn dưới ${this.config.maxAltLength} ký tự`
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (alt && alt.length < this.config.minAltLength && imageType !== 'decorative') {
|
|
126
|
-
issues.push({
|
|
127
|
-
type: 'ALT_TOO_SHORT',
|
|
128
|
-
severity: 'WARNING',
|
|
129
|
-
message: `Alt text quá ngắn (${alt.length} ký tự)`,
|
|
130
|
-
description: 'Alt text nên mô tả đầy đủ nội dung hình ảnh'
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 4. Kiểm tra chất lượng nội dung alt
|
|
135
|
-
const contentIssues = this.checkAltContent(alt, src, imageType);
|
|
136
|
-
issues.push(...contentIssues);
|
|
137
|
-
|
|
138
|
-
// 5. Kiểm tra tính nhất quán với các thuộc tính khác
|
|
139
|
-
const consistencyIssues = this.checkAttributeConsistency(analysis);
|
|
140
|
-
issues.push(...consistencyIssues);
|
|
141
|
-
|
|
142
|
-
// 6. Kiểm tra theo loại hình ảnh cụ thể
|
|
143
|
-
const typeSpecificIssues = this.checkTypeSpecificRequirements(analysis);
|
|
144
|
-
issues.push(...typeSpecificIssues);
|
|
145
|
-
|
|
146
|
-
return issues;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Phân loại hình ảnh theo mục đích sử dụng
|
|
151
|
-
*/
|
|
152
|
-
classifyImageType(imgTag, context, src) {
|
|
153
|
-
const srcLower = (src || '').toLowerCase();
|
|
154
|
-
const contextLower = context.toLowerCase();
|
|
155
|
-
|
|
156
|
-
// Hình trang trí
|
|
157
|
-
if (this.isDecorativeImage(imgTag, context, src)) {
|
|
158
|
-
return 'decorative';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Hình biểu đồ/dữ liệu
|
|
162
|
-
if (this.isDataVisualization(srcLower, contextLower)) {
|
|
163
|
-
return 'data-visualization';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Hình phức tạp (cần mô tả dài)
|
|
167
|
-
if (this.isComplexImage(srcLower, contextLower)) {
|
|
168
|
-
return 'complex';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Logo/thương hiệu
|
|
172
|
-
if (this.isLogo(srcLower, contextLower)) {
|
|
173
|
-
return 'logo';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Icon chức năng
|
|
177
|
-
if (this.isFunctionalIcon(imgTag, context, srcLower)) {
|
|
178
|
-
return 'functional-icon';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Hình ảnh nội dung
|
|
182
|
-
if (this.isContentImage(contextLower)) {
|
|
183
|
-
return 'content';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return 'informative';
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Kiểm tra nội dung alt text
|
|
191
|
-
*/
|
|
192
|
-
checkAltContent(alt, src, imageType) {
|
|
193
|
-
const issues = [];
|
|
194
|
-
|
|
195
|
-
if (!alt) return issues;
|
|
196
|
-
|
|
197
|
-
const altLower = alt.toLowerCase();
|
|
198
|
-
const srcLower = (src || '').toLowerCase();
|
|
199
|
-
|
|
200
|
-
// Kiểm tra từ khóa không nên có
|
|
201
|
-
const forbiddenWords = [
|
|
202
|
-
'image', 'picture', 'photo', 'graphic', 'img',
|
|
203
|
-
'画像', '写真', 'イメージ', '図', '図表'
|
|
204
|
-
];
|
|
205
|
-
|
|
206
|
-
const foundForbidden = forbiddenWords.find(word => altLower.includes(word));
|
|
207
|
-
if (foundForbidden) {
|
|
208
|
-
issues.push({
|
|
209
|
-
type: 'REDUNDANT_WORDS',
|
|
210
|
-
severity: 'WARNING',
|
|
211
|
-
message: `Alt text chứa từ thừa: "${foundForbidden}"`,
|
|
212
|
-
description: 'Không cần nói "hình ảnh" trong alt text'
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Kiểm tra lặp lại tên file
|
|
217
|
-
if (src) {
|
|
218
|
-
const filename = src.split('/').pop().split('.')[0];
|
|
219
|
-
if (altLower.includes(filename.toLowerCase())) {
|
|
220
|
-
issues.push({
|
|
221
|
-
type: 'FILENAME_IN_ALT',
|
|
222
|
-
severity: 'WARNING',
|
|
223
|
-
message: 'Alt text chứa tên file',
|
|
224
|
-
description: 'Nên mô tả nội dung thay vì tên file'
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Kiểm tra alt text chung chung
|
|
230
|
-
const genericTexts = [
|
|
231
|
-
'click here', 'read more', 'learn more', 'see more',
|
|
232
|
-
'ここをクリック', '詳細', 'もっと見る'
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
const foundGeneric = genericTexts.find(text => altLower.includes(text));
|
|
236
|
-
if (foundGeneric) {
|
|
237
|
-
issues.push({
|
|
238
|
-
type: 'GENERIC_ALT',
|
|
239
|
-
severity: 'ERROR',
|
|
240
|
-
message: `Alt text quá chung chung: "${foundGeneric}"`,
|
|
241
|
-
description: 'Nên mô tả cụ thể nội dung hình ảnh'
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Kiểm tra theo loại hình ảnh
|
|
246
|
-
if (imageType === 'data-visualization' && !this.hasDataDescription(alt)) {
|
|
247
|
-
issues.push({
|
|
248
|
-
type: 'MISSING_DATA_DESCRIPTION',
|
|
249
|
-
severity: 'ERROR',
|
|
250
|
-
message: 'Biểu đồ thiếu mô tả dữ liệu',
|
|
251
|
-
description: 'Biểu đồ cần mô tả xu hướng và dữ liệu chính'
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return issues;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Tạo đề xuất cải thiện alt text
|
|
260
|
-
*/
|
|
261
|
-
generateRecommendations(imgTag, analysis) {
|
|
262
|
-
const recommendations = [];
|
|
263
|
-
const { imageType, context, src, alt } = analysis;
|
|
264
|
-
|
|
265
|
-
switch (imageType) {
|
|
266
|
-
case 'decorative':
|
|
267
|
-
recommendations.push({
|
|
268
|
-
type: 'DECORATIVE',
|
|
269
|
-
suggestion: 'alt=""',
|
|
270
|
-
reason: 'Hình trang trí nên có alt rỗng'
|
|
271
|
-
});
|
|
272
|
-
break;
|
|
273
|
-
|
|
274
|
-
case 'logo':
|
|
275
|
-
const brandName = this.extractBrandName(context, src);
|
|
276
|
-
recommendations.push({
|
|
277
|
-
type: 'LOGO',
|
|
278
|
-
suggestion: brandName ? `alt="${brandName} logo"` : 'alt="Company logo"',
|
|
279
|
-
reason: 'Logo nên bao gồm tên thương hiệu'
|
|
280
|
-
});
|
|
281
|
-
break;
|
|
282
|
-
|
|
283
|
-
case 'functional-icon':
|
|
284
|
-
const action = this.extractIconAction(context, imgTag);
|
|
285
|
-
recommendations.push({
|
|
286
|
-
type: 'FUNCTIONAL',
|
|
287
|
-
suggestion: action ? `alt="${action}"` : 'alt="[Mô tả chức năng]"',
|
|
288
|
-
reason: 'Icon chức năng nên mô tả hành động'
|
|
289
|
-
});
|
|
290
|
-
break;
|
|
291
|
-
|
|
292
|
-
case 'data-visualization':
|
|
293
|
-
recommendations.push({
|
|
294
|
-
type: 'DATA_VIZ',
|
|
295
|
-
suggestion: 'alt="[Loại biểu đồ]: [Xu hướng chính] [Dữ liệu quan trọng]"',
|
|
296
|
-
reason: 'Biểu đồ cần mô tả loại, xu hướng và dữ liệu chính'
|
|
297
|
-
});
|
|
298
|
-
break;
|
|
299
|
-
|
|
300
|
-
case 'complex':
|
|
301
|
-
recommendations.push({
|
|
302
|
-
type: 'COMPLEX',
|
|
303
|
-
suggestion: 'alt="[Mô tả ngắn]" + longdesc hoặc mô tả chi tiết bên dưới',
|
|
304
|
-
reason: 'Hình phức tạp cần mô tả ngắn trong alt và mô tả dài riêng'
|
|
305
|
-
});
|
|
306
|
-
break;
|
|
307
|
-
|
|
308
|
-
default:
|
|
309
|
-
const contextualAlt = this.generateContextualAlt(analysis);
|
|
310
|
-
recommendations.push({
|
|
311
|
-
type: 'CONTEXTUAL',
|
|
312
|
-
suggestion: `alt="${contextualAlt}"`,
|
|
313
|
-
reason: 'Mô tả dựa trên ngữ cảnh xung quanh'
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return recommendations;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Tạo alt text dựa trên ngữ cảnh
|
|
322
|
-
*/
|
|
323
|
-
generateContextualAlt(analysis) {
|
|
324
|
-
const { context, src, structural } = analysis;
|
|
325
|
-
|
|
326
|
-
// Tìm tiêu đề gần nhất
|
|
327
|
-
const nearbyHeading = this.findNearbyHeading(context);
|
|
328
|
-
if (nearbyHeading) {
|
|
329
|
-
return nearbyHeading;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Tìm text trong link chứa hình
|
|
333
|
-
if (structural.parentLink) {
|
|
334
|
-
const linkText = this.extractLinkText(structural.parentLink);
|
|
335
|
-
if (linkText) {
|
|
336
|
-
return linkText;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Tìm figcaption
|
|
341
|
-
if (structural.figcaption) {
|
|
342
|
-
return structural.figcaption;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Tìm text xung quanh
|
|
346
|
-
const surroundingText = this.extractSurroundingText(context);
|
|
347
|
-
if (surroundingText) {
|
|
348
|
-
return surroundingText;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Fallback dựa trên src
|
|
352
|
-
return this.generateFallbackAlt(src);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Helper methods cho việc phân loại hình ảnh
|
|
356
|
-
isDecorativeImage(imgTag, context, src) {
|
|
357
|
-
const decorativeIndicators = [
|
|
358
|
-
'decoration', 'border', 'spacer', 'divider',
|
|
359
|
-
'background', 'texture', 'pattern'
|
|
360
|
-
];
|
|
361
|
-
|
|
362
|
-
const srcLower = (src || '').toLowerCase();
|
|
363
|
-
return decorativeIndicators.some(indicator => srcLower.includes(indicator));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
isDataVisualization(src, context) {
|
|
367
|
-
const dataIndicators = [
|
|
368
|
-
'chart', 'graph', 'plot', 'diagram', 'infographic',
|
|
369
|
-
'グラフ', '図表', 'チャート'
|
|
370
|
-
];
|
|
371
|
-
|
|
372
|
-
return dataIndicators.some(indicator =>
|
|
373
|
-
src.includes(indicator) || context.includes(indicator)
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
isComplexImage(src, context) {
|
|
378
|
-
const complexIndicators = [
|
|
379
|
-
'flowchart', 'timeline', 'map', 'blueprint', 'schematic',
|
|
380
|
-
'フローチャート', '地図', '設計図'
|
|
381
|
-
];
|
|
382
|
-
|
|
383
|
-
return complexIndicators.some(indicator =>
|
|
384
|
-
src.includes(indicator) || context.includes(indicator)
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
isLogo(src, context) {
|
|
389
|
-
const logoIndicators = ['logo', 'brand', 'ロゴ', 'ブランド'];
|
|
390
|
-
return logoIndicators.some(indicator =>
|
|
391
|
-
src.includes(indicator) || context.includes(indicator)
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
isFunctionalIcon(imgTag, context, src) {
|
|
396
|
-
const iconIndicators = ['icon', 'btn', 'button', 'アイコン', 'ボタン'];
|
|
397
|
-
const hasClickHandler = /onclick|href/i.test(context);
|
|
398
|
-
|
|
399
|
-
return (iconIndicators.some(indicator => src.includes(indicator)) || hasClickHandler);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
isContentImage(context) {
|
|
403
|
-
const contentIndicators = [
|
|
404
|
-
'article', 'content', 'story', 'news',
|
|
405
|
-
'記事', 'コンテンツ', 'ニュース'
|
|
406
|
-
];
|
|
407
|
-
|
|
408
|
-
return contentIndicators.some(indicator => context.includes(indicator));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Helper methods khác
|
|
412
|
-
extractAttribute(imgTag, attributeName) {
|
|
413
|
-
const regex = new RegExp(`${attributeName}\\s*=\\s*["']([^"']*)["']`, 'i');
|
|
414
|
-
const match = imgTag.match(regex);
|
|
415
|
-
return match ? match[1] : null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
hasAttribute(imgTag, attributeName) {
|
|
419
|
-
const regex = new RegExp(`${attributeName}\\s*=`, 'i');
|
|
420
|
-
return regex.test(imgTag);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
findImagePosition(imgTag, htmlContent, imgIndex) {
|
|
424
|
-
const imgRegex = /<img[^>]*>/gi;
|
|
425
|
-
let match;
|
|
426
|
-
let currentIndex = 0;
|
|
427
|
-
|
|
428
|
-
while ((match = imgRegex.exec(htmlContent)) !== null) {
|
|
429
|
-
if (currentIndex === imgIndex) {
|
|
430
|
-
return match.index;
|
|
431
|
-
}
|
|
432
|
-
currentIndex++;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return -1;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
extractSurroundingContext(htmlContent, position, range) {
|
|
439
|
-
if (position === -1) return '';
|
|
440
|
-
|
|
441
|
-
const start = Math.max(0, position - range);
|
|
442
|
-
const end = Math.min(htmlContent.length, position + range);
|
|
443
|
-
|
|
444
|
-
return htmlContent.substring(start, end);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
analyzeStructuralContext(context, imgTag) {
|
|
448
|
-
return {
|
|
449
|
-
parentLink: this.findParentElement(context, imgTag, 'a'),
|
|
450
|
-
parentFigure: this.findParentElement(context, imgTag, 'figure'),
|
|
451
|
-
figcaption: this.findSiblingElement(context, imgTag, 'figcaption'),
|
|
452
|
-
parentButton: this.findParentElement(context, imgTag, 'button')
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
findParentElement(context, imgTag, tagName) {
|
|
457
|
-
const imgIndex = context.indexOf(imgTag);
|
|
458
|
-
if (imgIndex === -1) return null;
|
|
459
|
-
|
|
460
|
-
// Tìm thẻ mở gần nhất trước img
|
|
461
|
-
const beforeImg = context.substring(0, imgIndex);
|
|
462
|
-
const openTagRegex = new RegExp(`<${tagName}[^>]*>`, 'gi');
|
|
463
|
-
const closeTagRegex = new RegExp(`</${tagName}>`, 'gi');
|
|
464
|
-
|
|
465
|
-
let openTags = 0;
|
|
466
|
-
let lastOpenMatch = null;
|
|
467
|
-
|
|
468
|
-
// Đếm thẻ mở và đóng để tìm parent
|
|
469
|
-
let match;
|
|
470
|
-
while ((match = openTagRegex.exec(beforeImg)) !== null) {
|
|
471
|
-
lastOpenMatch = match;
|
|
472
|
-
openTags++;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
while ((match = closeTagRegex.exec(beforeImg)) !== null) {
|
|
476
|
-
openTags--;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return openTags > 0 ? lastOpenMatch[0] : null;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
findSiblingElement(context, imgTag, tagName) {
|
|
483
|
-
const imgIndex = context.indexOf(imgTag);
|
|
484
|
-
if (imgIndex === -1) return null;
|
|
485
|
-
|
|
486
|
-
const afterImg = context.substring(imgIndex + imgTag.length);
|
|
487
|
-
const siblingRegex = new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`, 'i');
|
|
488
|
-
const match = afterImg.match(siblingRegex);
|
|
489
|
-
|
|
490
|
-
return match ? match[1].trim() : null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
checkAttributeConsistency(analysis) {
|
|
494
|
-
const issues = [];
|
|
495
|
-
const { alt, title, ariaLabel } = analysis;
|
|
496
|
-
|
|
497
|
-
// Kiểm tra tính nhất quán giữa alt và aria-label
|
|
498
|
-
if (alt && ariaLabel && alt !== ariaLabel) {
|
|
499
|
-
issues.push({
|
|
500
|
-
type: 'INCONSISTENT_LABELS',
|
|
501
|
-
severity: 'WARNING',
|
|
502
|
-
message: 'Alt text và aria-label không nhất quán',
|
|
503
|
-
description: 'Alt và aria-label nên có nội dung giống nhau'
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Kiểm tra title attribute
|
|
508
|
-
if (title && alt && title === alt) {
|
|
509
|
-
issues.push({
|
|
510
|
-
type: 'REDUNDANT_TITLE',
|
|
511
|
-
severity: 'INFO',
|
|
512
|
-
message: 'Title attribute trùng với alt text',
|
|
513
|
-
description: 'Title có thể bỏ đi để tránh lặp lại'
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return issues;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
checkTypeSpecificRequirements(analysis) {
|
|
521
|
-
const issues = [];
|
|
522
|
-
const { imageType, alt, structural } = analysis;
|
|
523
|
-
|
|
524
|
-
switch (imageType) {
|
|
525
|
-
case 'functional-icon':
|
|
526
|
-
if (structural.parentLink && !alt) {
|
|
527
|
-
issues.push({
|
|
528
|
-
type: 'FUNCTIONAL_MISSING_ALT',
|
|
529
|
-
severity: 'ERROR',
|
|
530
|
-
message: 'Icon chức năng trong link thiếu alt text',
|
|
531
|
-
description: 'Icon có chức năng phải có alt mô tả hành động'
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
break;
|
|
535
|
-
|
|
536
|
-
case 'logo':
|
|
537
|
-
if (alt && !alt.toLowerCase().includes('logo')) {
|
|
538
|
-
issues.push({
|
|
539
|
-
type: 'LOGO_MISSING_CONTEXT',
|
|
540
|
-
severity: 'WARNING',
|
|
541
|
-
message: 'Logo thiếu từ khóa "logo" trong alt text',
|
|
542
|
-
description: 'Logo nên bao gồm từ "logo" để rõ ràng'
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return issues;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
hasDataDescription(alt) {
|
|
552
|
-
const dataKeywords = [
|
|
553
|
-
'increase', 'decrease', 'trend', 'percent', '%',
|
|
554
|
-
'増加', '減少', 'トレンド', 'パーセント'
|
|
555
|
-
];
|
|
556
|
-
|
|
557
|
-
return dataKeywords.some(keyword =>
|
|
558
|
-
alt.toLowerCase().includes(keyword.toLowerCase())
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Các method helper khác...
|
|
563
|
-
findNearbyHeading(context) {
|
|
564
|
-
const headingRegex = /<h[1-6][^>]*>([^<]+)<\/h[1-6]>/gi;
|
|
565
|
-
const match = headingRegex.exec(context);
|
|
566
|
-
return match ? match[1].trim() : null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
extractLinkText(linkTag) {
|
|
570
|
-
const textMatch = linkTag.match(/>([^<]+)</);
|
|
571
|
-
return textMatch ? textMatch[1].trim() : null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
extractSurroundingText(context) {
|
|
575
|
-
// Loại bỏ HTML tags và lấy text xung quanh
|
|
576
|
-
const textOnly = context.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
577
|
-
const words = textOnly.split(' ');
|
|
578
|
-
|
|
579
|
-
// Lấy 3-5 từ có nghĩa
|
|
580
|
-
const meaningfulWords = words.filter(word =>
|
|
581
|
-
word.length > 2 && !/^\d+$/.test(word)
|
|
582
|
-
).slice(0, 5);
|
|
583
|
-
|
|
584
|
-
return meaningfulWords.join(' ');
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
generateFallbackAlt(src) {
|
|
588
|
-
if (!src) return '画像';
|
|
589
|
-
|
|
590
|
-
const filename = src.split('/').pop().split('.')[0];
|
|
591
|
-
|
|
592
|
-
// Cải thiện tên file thành alt text
|
|
593
|
-
return filename
|
|
594
|
-
.replace(/[-_]/g, ' ')
|
|
595
|
-
.replace(/\b\w/g, l => l.toUpperCase())
|
|
596
|
-
.trim() || '画像';
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
extractBrandName(context, src) {
|
|
600
|
-
// Tìm tên thương hiệu từ context hoặc src
|
|
601
|
-
const brandPatterns = [
|
|
602
|
-
/company[^>]*>([^<]+)/i,
|
|
603
|
-
/brand[^>]*>([^<]+)/i,
|
|
604
|
-
/logo[^>]*>([^<]+)/i
|
|
605
|
-
];
|
|
606
|
-
|
|
607
|
-
for (const pattern of brandPatterns) {
|
|
608
|
-
const match = context.match(pattern);
|
|
609
|
-
if (match) return match[1].trim();
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
extractIconAction(context, imgTag) {
|
|
616
|
-
// Tìm hành động từ context xung quanh icon
|
|
617
|
-
const actionPatterns = [
|
|
618
|
-
/title\s*=\s*["']([^"']+)["']/i,
|
|
619
|
-
/aria-label\s*=\s*["']([^"']+)["']/i,
|
|
620
|
-
/onclick[^>]*>([^<]+)/i
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
for (const pattern of actionPatterns) {
|
|
624
|
-
const match = imgTag.match(pattern) || context.match(pattern);
|
|
625
|
-
if (match) return match[1].trim();
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
module.exports = EnhancedAltChecker;
|