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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "gbu-accessibility-package",
3
- "version": "3.2.0",
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": "./cli.js",
8
- "accessibility-fixer": "./cli.js"
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;