@kodalabs-io/eqo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,941 @@
1
+ import {
2
+ ALL_CRITERIA,
3
+ AXE_TEST_ID_PREFIX,
4
+ AXE_TO_RGAA,
5
+ AnalysisError,
6
+ CRITERIA_BY_ID,
7
+ ConfigError,
8
+ KodaRGAAConfigSchema,
9
+ THEMES,
10
+ TOTAL_CRITERIA,
11
+ TimeoutError,
12
+ analyze,
13
+ buildReport,
14
+ createStaticIssue,
15
+ defineRule,
16
+ generateDefaultConfig,
17
+ getAttr,
18
+ getAttrMap,
19
+ getAttrStringValue,
20
+ getAxeRulesForCriterion,
21
+ getRGAACriteria,
22
+ getSupportedLocales,
23
+ getTagName,
24
+ getTextContent,
25
+ getTranslations,
26
+ interpolate,
27
+ isAttrDynamic,
28
+ issueId,
29
+ loadConfig,
30
+ loadTranslations,
31
+ printReport,
32
+ resolveConfigPath,
33
+ runRuntimeAnalysis,
34
+ runStaticAnalysis,
35
+ walk,
36
+ writeHtmlReport,
37
+ writeJsonReport,
38
+ writeJunitReport,
39
+ writeMarkdownReport,
40
+ writeReports,
41
+ writeSarifReport
42
+ } from "./chunk-WKI3N5NX.js";
43
+
44
+ // src/analyzer/static/rules/01-images.ts
45
+ var imagesNeedAlt = defineRule({ id: "images/alt-present", criteria: ["1.1"] }, (context) => {
46
+ const { filePath, ast } = context;
47
+ const issues = [];
48
+ walk(ast, {
49
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: covers img, input[type=image], svg[role=img], and generic role=img elements
50
+ JSXOpeningElement(rawNode) {
51
+ const node = rawNode;
52
+ const tag = getTagName(node);
53
+ const attrs = getAttrMap(node);
54
+ if (tag === "img") {
55
+ if (!attrs.has("alt") && !attrs.has("aria-label") && !attrs.has("aria-labelledby")) {
56
+ issues.push(
57
+ createStaticIssue({
58
+ node,
59
+ filePath,
60
+ criterionId: "1.1",
61
+ testId: "1.1.1",
62
+ messageKey: "img.missing-alt",
63
+ wcag: "1.1.1"
64
+ })
65
+ );
66
+ }
67
+ return;
68
+ }
69
+ if (tag === "input") {
70
+ const typeVal = getAttrStringValue(attrs.get("type"));
71
+ if (typeVal === "image") {
72
+ if (!attrs.has("alt") && !attrs.has("aria-label") && !attrs.has("aria-labelledby")) {
73
+ issues.push(
74
+ createStaticIssue({
75
+ node,
76
+ filePath,
77
+ criterionId: "1.1",
78
+ testId: "1.1.3",
79
+ messageKey: "img.input-image-missing-alt",
80
+ wcag: "1.1.1"
81
+ })
82
+ );
83
+ }
84
+ }
85
+ return;
86
+ }
87
+ if (tag === "svg") {
88
+ const roleVal2 = getAttrStringValue(attrs.get("role"));
89
+ if (roleVal2 === "img") {
90
+ if (!attrs.has("aria-label") && !attrs.has("aria-labelledby")) {
91
+ issues.push(
92
+ createStaticIssue({
93
+ node,
94
+ filePath,
95
+ criterionId: "1.1",
96
+ testId: "1.1.5",
97
+ messageKey: "img.svg-missing-accessible-name",
98
+ wcag: "1.1.1"
99
+ })
100
+ );
101
+ }
102
+ }
103
+ return;
104
+ }
105
+ const roleVal = getAttrStringValue(attrs.get("role"));
106
+ if (roleVal === "img") {
107
+ if (!attrs.has("aria-label") && !attrs.has("aria-labelledby")) {
108
+ issues.push(
109
+ createStaticIssue({
110
+ node,
111
+ filePath,
112
+ criterionId: "1.1",
113
+ testId: "1.1.1",
114
+ messageKey: "img.missing-alt-on-role-img",
115
+ wcag: "1.1.1"
116
+ })
117
+ );
118
+ }
119
+ }
120
+ }
121
+ });
122
+ return issues;
123
+ });
124
+ var decorativeSvgMustBeHidden = defineRule(
125
+ { id: "images/decorative-svg-hidden", criteria: ["1.2"] },
126
+ (context) => {
127
+ const { filePath, ast } = context;
128
+ const issues = [];
129
+ walk(ast, {
130
+ JSXOpeningElement(rawNode) {
131
+ const node = rawNode;
132
+ if (getTagName(node) !== "svg") return;
133
+ const role = getAttrStringValue(getAttr(node, "role"));
134
+ const ariaHidden = getAttrStringValue(getAttr(node, "aria-hidden"));
135
+ const ariaLabel = getAttr(node, "aria-label");
136
+ const ariaLabelledby = getAttr(node, "aria-labelledby");
137
+ if (role !== "img" && ariaHidden !== "true" && !ariaLabel && !ariaLabelledby) {
138
+ issues.push(
139
+ createStaticIssue({
140
+ node,
141
+ filePath,
142
+ criterionId: "1.2",
143
+ testId: "1.2.4",
144
+ severity: "notice",
145
+ messageKey: "img.decorative-svg-not-hidden",
146
+ wcag: "1.1.1"
147
+ })
148
+ );
149
+ }
150
+ }
151
+ });
152
+ return issues;
153
+ }
154
+ );
155
+ var figcaptionInFigure = defineRule(
156
+ { id: "images/figcaption-in-figure", criteria: ["1.9"] },
157
+ (context) => {
158
+ const { filePath, ast } = context;
159
+ const issues = [];
160
+ walk(ast, {
161
+ JSXElement(rawNode) {
162
+ const node = rawNode;
163
+ const opening = node.openingElement;
164
+ if (getTagName(opening) !== "figure") return;
165
+ let hasFigcaption = false;
166
+ let hasImage = false;
167
+ for (const child of node.children) {
168
+ if (child.type !== "JSXElement") continue;
169
+ const childTag = getTagName(child.openingElement);
170
+ if (childTag === "figcaption") hasFigcaption = true;
171
+ if (childTag === "img" || childTag === "picture" || childTag === "svg" || childTag === "canvas") {
172
+ hasImage = true;
173
+ }
174
+ }
175
+ if (hasFigcaption && !hasImage) {
176
+ issues.push(
177
+ createStaticIssue({
178
+ node: opening,
179
+ filePath,
180
+ criterionId: "1.9",
181
+ testId: "1.9.1",
182
+ severity: "warning",
183
+ messageKey: "img.figure-missing-img",
184
+ remediationKey: "img.missing-alt",
185
+ wcag: "1.1.1"
186
+ })
187
+ );
188
+ }
189
+ }
190
+ });
191
+ return issues;
192
+ }
193
+ );
194
+ var imageRules = [imagesNeedAlt, decorativeSvgMustBeHidden, figcaptionInFigure];
195
+
196
+ // src/analyzer/static/rules/02-frames.ts
197
+ var framesNeedTitle = defineRule(
198
+ { id: "frames/title-present", criteria: ["2.1", "2.2"] },
199
+ (context) => {
200
+ const { filePath, ast } = context;
201
+ const issues = [];
202
+ walk(ast, {
203
+ JSXOpeningElement(rawNode) {
204
+ const node = rawNode;
205
+ if (getTagName(node) !== "iframe") return;
206
+ const titleAttr = getAttr(node, "title");
207
+ if (!titleAttr) {
208
+ issues.push(
209
+ createStaticIssue({
210
+ node,
211
+ filePath,
212
+ criterionId: "2.1",
213
+ testId: "2.1.1",
214
+ messageKey: "frame.missing-title",
215
+ wcag: "4.1.2"
216
+ })
217
+ );
218
+ return;
219
+ }
220
+ if (isAttrDynamic(titleAttr)) return;
221
+ const titleValue = getAttrStringValue(titleAttr);
222
+ if (titleValue !== null && titleValue.trim() === "") {
223
+ issues.push(
224
+ createStaticIssue({
225
+ node,
226
+ filePath,
227
+ criterionId: "2.2",
228
+ testId: "2.2.1",
229
+ messageKey: "frame.empty-title",
230
+ wcag: "4.1.2"
231
+ })
232
+ );
233
+ }
234
+ }
235
+ });
236
+ return issues;
237
+ }
238
+ );
239
+ var frameRules = [framesNeedTitle];
240
+
241
+ // src/analyzer/static/rules/05-tables.ts
242
+ var tablesNeedCaption = defineRule(
243
+ { id: "tables/caption-present", criteria: ["5.4"] },
244
+ (context) => {
245
+ const { filePath, ast } = context;
246
+ const issues = [];
247
+ walk(ast, {
248
+ JSXElement(rawNode) {
249
+ const node = rawNode;
250
+ const opening = node.openingElement;
251
+ if (getTagName(opening) !== "table") return;
252
+ const role = getAttrStringValue(getAttr(opening, "role"));
253
+ if (role === "presentation" || role === "none") return;
254
+ if (getAttr(opening, "aria-label") || getAttr(opening, "aria-labelledby")) return;
255
+ const hasCaption = node.children.some(
256
+ (child) => child.type === "JSXElement" && getTagName(child.openingElement) === "caption"
257
+ );
258
+ if (!hasCaption) {
259
+ issues.push(
260
+ createStaticIssue({
261
+ node: opening,
262
+ filePath,
263
+ criterionId: "5.4",
264
+ testId: "5.4.1",
265
+ severity: "warning",
266
+ messageKey: "table.missing-caption",
267
+ wcag: "1.3.1"
268
+ })
269
+ );
270
+ }
271
+ }
272
+ });
273
+ return issues;
274
+ }
275
+ );
276
+ var thNeedsScope = defineRule({ id: "tables/th-scope", criteria: ["5.6"] }, (context) => {
277
+ const { filePath, ast } = context;
278
+ const issues = [];
279
+ walk(ast, {
280
+ JSXOpeningElement(rawNode) {
281
+ const node = rawNode;
282
+ if (getTagName(node) !== "th") return;
283
+ const scope = getAttr(node, "scope");
284
+ const id = getAttr(node, "id");
285
+ if (!scope && !id) {
286
+ issues.push(
287
+ createStaticIssue({
288
+ node,
289
+ filePath,
290
+ criterionId: "5.6",
291
+ testId: "5.6.1",
292
+ severity: "warning",
293
+ messageKey: "table.th-missing-scope",
294
+ wcag: "1.3.1"
295
+ })
296
+ );
297
+ }
298
+ }
299
+ });
300
+ return issues;
301
+ });
302
+ var layoutTableNoStructure = defineRule(
303
+ { id: "tables/layout-no-structural-elements", criteria: ["5.8"] },
304
+ (context) => {
305
+ const { filePath, ast } = context;
306
+ const issues = [];
307
+ walk(ast, {
308
+ JSXElement(rawNode) {
309
+ const node = rawNode;
310
+ const opening = node.openingElement;
311
+ if (getTagName(opening) !== "table") return;
312
+ const role = getAttrStringValue(getAttr(opening, "role"));
313
+ if (role !== "presentation" && role !== "none") return;
314
+ for (const child of node.children) {
315
+ if (child.type !== "JSXElement") continue;
316
+ const childTag = getTagName(child.openingElement);
317
+ if (childTag === "th") {
318
+ issues.push(
319
+ createStaticIssue({
320
+ node: child.openingElement,
321
+ filePath,
322
+ criterionId: "5.8",
323
+ testId: "5.8.1",
324
+ messageKey: "table.layout-has-th",
325
+ wcag: "1.3.1"
326
+ })
327
+ );
328
+ }
329
+ if (childTag === "caption") {
330
+ issues.push(
331
+ createStaticIssue({
332
+ node: child.openingElement,
333
+ filePath,
334
+ criterionId: "5.8",
335
+ testId: "5.8.1",
336
+ messageKey: "table.layout-has-caption",
337
+ remediationKey: "table.missing-caption",
338
+ wcag: "1.3.1"
339
+ })
340
+ );
341
+ }
342
+ }
343
+ }
344
+ });
345
+ return issues;
346
+ }
347
+ );
348
+ var tableRules = [tablesNeedCaption, thNeedsScope, layoutTableNoStructure];
349
+
350
+ // src/analyzer/static/rules/06-links.ts
351
+ var linksNeedLabel = defineRule({ id: "links/label-present", criteria: ["6.2"] }, (context) => {
352
+ const { filePath, ast } = context;
353
+ const issues = [];
354
+ walk(ast, {
355
+ JSXElement(rawNode) {
356
+ const node = rawNode;
357
+ const opening = node.openingElement;
358
+ if (getTagName(opening) !== "a") return;
359
+ const ariaLabel = getAttr(opening, "aria-label");
360
+ const ariaLabelledby = getAttr(opening, "aria-labelledby");
361
+ if (ariaLabel) {
362
+ const val = getAttrStringValue(ariaLabel);
363
+ if (val !== null && val.trim() === "") {
364
+ issues.push(
365
+ createStaticIssue({
366
+ node: opening,
367
+ filePath,
368
+ criterionId: "6.2",
369
+ testId: "6.2.1",
370
+ messageKey: "link.empty-label",
371
+ remediationKey: "link.missing-label",
372
+ wcag: "4.1.2"
373
+ })
374
+ );
375
+ }
376
+ return;
377
+ }
378
+ if (ariaLabelledby || isAttrDynamic(ariaLabel)) return;
379
+ const text = getTextContent(node);
380
+ if (text === null) return;
381
+ if (text.trim() === "") {
382
+ issues.push(
383
+ createStaticIssue({
384
+ node: opening,
385
+ filePath,
386
+ criterionId: "6.2",
387
+ testId: "6.2.1",
388
+ messageKey: "link.missing-label",
389
+ wcag: "4.1.2"
390
+ })
391
+ );
392
+ }
393
+ }
394
+ });
395
+ return issues;
396
+ });
397
+ var linkRules = [linksNeedLabel];
398
+
399
+ // src/analyzer/static/rules/08-mandatory.ts
400
+ import path from "path";
401
+ var VALID_LANG_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
402
+ var LAYOUT_FILE_NAMES = /* @__PURE__ */ new Set(["layout", "_document", "RootLayout", "Head"]);
403
+ function isLayoutFile(filePath) {
404
+ return LAYOUT_FILE_NAMES.has(path.basename(filePath, path.extname(filePath)));
405
+ }
406
+ var htmlNeedsLang = defineRule(
407
+ { id: "mandatory/html-lang", criteria: ["8.3", "8.4"] },
408
+ (context) => {
409
+ const { filePath, ast } = context;
410
+ if (!isLayoutFile(filePath) || path.basename(filePath, path.extname(filePath)) === "Head")
411
+ return [];
412
+ const issues = [];
413
+ walk(ast, {
414
+ JSXOpeningElement(rawNode) {
415
+ const node = rawNode;
416
+ if (getTagName(node) !== "html") return;
417
+ const langAttr = getAttr(node, "lang");
418
+ if (!langAttr) {
419
+ issues.push(
420
+ createStaticIssue({
421
+ node,
422
+ filePath,
423
+ criterionId: "8.3",
424
+ testId: "8.3.1",
425
+ messageKey: "html.missing-lang",
426
+ wcag: "3.1.1"
427
+ })
428
+ );
429
+ return;
430
+ }
431
+ if (isAttrDynamic(langAttr)) return;
432
+ const langValue = getAttrStringValue(langAttr);
433
+ if (langValue !== null && langValue.trim() === "") {
434
+ issues.push(
435
+ createStaticIssue({
436
+ node,
437
+ filePath,
438
+ criterionId: "8.3",
439
+ testId: "8.3.1",
440
+ messageKey: "html.empty-lang",
441
+ remediationKey: "html.missing-lang",
442
+ wcag: "3.1.1"
443
+ })
444
+ );
445
+ return;
446
+ }
447
+ if (langValue !== null && !VALID_LANG_RE.test(langValue)) {
448
+ issues.push(
449
+ createStaticIssue({
450
+ node,
451
+ filePath,
452
+ criterionId: "8.4",
453
+ testId: "8.4.1",
454
+ messageKey: "html.invalid-lang",
455
+ wcag: "3.1.1",
456
+ messageContext: { lang: langValue }
457
+ })
458
+ );
459
+ }
460
+ }
461
+ });
462
+ return issues;
463
+ }
464
+ );
465
+ var pageNeedsTitle = defineRule(
466
+ { id: "mandatory/page-title", criteria: ["8.5", "8.6"] },
467
+ (context) => {
468
+ const { filePath, ast } = context;
469
+ if (!isLayoutFile(filePath)) return [];
470
+ const issues = [];
471
+ let foundTitle = false;
472
+ walk(ast, {
473
+ JSXOpeningElement(rawNode) {
474
+ const node = rawNode;
475
+ if (getTagName(node) !== "title") return;
476
+ foundTitle = true;
477
+ }
478
+ });
479
+ if (!foundTitle) {
480
+ issues.push({
481
+ id: issueId(),
482
+ criterionId: "8.5",
483
+ testId: "8.5.1",
484
+ phase: "static",
485
+ severity: "error",
486
+ file: filePath,
487
+ messageKey: "html.missing-title",
488
+ remediationKey: "html.missing-title",
489
+ wcag: "2.4.2"
490
+ });
491
+ }
492
+ return issues;
493
+ }
494
+ );
495
+ var PRESENTATIONAL_TAGS = /* @__PURE__ */ new Set(["b", "i", "u", "s", "blink", "marquee", "font", "center"]);
496
+ var noPresentationalTags = defineRule(
497
+ { id: "mandatory/no-presentational-tags", criteria: ["8.9"] },
498
+ (context) => {
499
+ const { filePath, ast } = context;
500
+ const issues = [];
501
+ walk(ast, {
502
+ JSXOpeningElement(rawNode) {
503
+ const node = rawNode;
504
+ const tag = getTagName(node);
505
+ if (!tag || !PRESENTATIONAL_TAGS.has(tag)) return;
506
+ const severity = tag === "b" || tag === "i" || tag === "s" ? "warning" : "error";
507
+ issues.push(
508
+ createStaticIssue({
509
+ node,
510
+ filePath,
511
+ criterionId: "8.9",
512
+ testId: "8.9.1",
513
+ severity,
514
+ messageKey: "html.presentational-tag",
515
+ wcag: "1.3.1",
516
+ messageContext: { tag }
517
+ })
518
+ );
519
+ }
520
+ });
521
+ return issues;
522
+ }
523
+ );
524
+ var mandatoryRules = [htmlNeedsLang, pageNeedsTitle, noPresentationalTags];
525
+
526
+ // src/analyzer/static/rules/09-structure.ts
527
+ var HEADING_TAGS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
528
+ var HEADING_LEVEL = {
529
+ h1: 1,
530
+ h2: 2,
531
+ h3: 3,
532
+ h4: 4,
533
+ h5: 5,
534
+ h6: 6
535
+ };
536
+ function headingLevel(tag) {
537
+ return HEADING_LEVEL[tag] ?? 0;
538
+ }
539
+ var headingHierarchy = defineRule(
540
+ { id: "structure/heading-hierarchy", criteria: ["9.1"] },
541
+ (context) => {
542
+ const { filePath, ast } = context;
543
+ const issues = [];
544
+ const headings = [];
545
+ walk(ast, {
546
+ JSXOpeningElement(rawNode) {
547
+ const node = rawNode;
548
+ const tag = getTagName(node);
549
+ if (tag && HEADING_TAGS.has(tag)) {
550
+ headings.push({ tag, node });
551
+ }
552
+ }
553
+ });
554
+ if (headings.length === 0) return issues;
555
+ let h1Count = 0;
556
+ for (const { tag, node } of headings) {
557
+ if (tag === "h1" && ++h1Count > 1) {
558
+ issues.push(
559
+ createStaticIssue({
560
+ node,
561
+ filePath,
562
+ criterionId: "9.1",
563
+ testId: "9.1.3",
564
+ messageKey: "heading.multiple-h1",
565
+ wcag: "1.3.1"
566
+ })
567
+ );
568
+ }
569
+ }
570
+ for (let i = 1; i < headings.length; i++) {
571
+ const prev = headings[i - 1];
572
+ const curr = headings[i];
573
+ if (!prev || !curr) continue;
574
+ const prevLevel = headingLevel(prev.tag);
575
+ const currLevel = headingLevel(curr.tag);
576
+ if (currLevel > prevLevel + 1) {
577
+ issues.push(
578
+ createStaticIssue({
579
+ node: curr.node,
580
+ filePath,
581
+ criterionId: "9.1",
582
+ testId: "9.1.2",
583
+ messageKey: "heading.skipped-level",
584
+ wcag: "1.3.1",
585
+ messageContext: { from: String(prevLevel), to: String(currLevel) }
586
+ })
587
+ );
588
+ }
589
+ }
590
+ return issues;
591
+ }
592
+ );
593
+ var ORDERED_LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol"]);
594
+ var listsProperlyStructured = defineRule(
595
+ { id: "structure/lists-structure", criteria: ["9.3"] },
596
+ (context) => {
597
+ const { filePath, ast } = context;
598
+ const issues = [];
599
+ walk(ast, {
600
+ JSXElement(rawNode) {
601
+ const node = rawNode;
602
+ const opening = node.openingElement;
603
+ const tag = getTagName(opening);
604
+ if (!tag || !ORDERED_LIST_TAGS.has(tag)) return;
605
+ const role = getAttrStringValue(getAttr(opening, "role"));
606
+ if (role === "none" || role === "presentation") return;
607
+ for (const child of node.children) {
608
+ if (child.type === "JSXText" && child.value.trim() === "") continue;
609
+ if (child.type !== "JSXElement") continue;
610
+ const childTag = getTagName(child.openingElement);
611
+ if (childTag !== null && childTag !== "li" && !/^[A-Z]/.test(childTag) && !childTag.includes("-")) {
612
+ issues.push(
613
+ createStaticIssue({
614
+ node: child.openingElement,
615
+ filePath,
616
+ criterionId: "9.3",
617
+ testId: "9.3.1",
618
+ messageKey: "list.invalid-child",
619
+ wcag: "1.3.1",
620
+ messageContext: { parent: tag, child: childTag }
621
+ })
622
+ );
623
+ }
624
+ }
625
+ }
626
+ });
627
+ return issues;
628
+ }
629
+ );
630
+ var structureRules = [headingHierarchy, listsProperlyStructured];
631
+
632
+ // src/analyzer/static/rules/11-forms.ts
633
+ var LABELABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
634
+ "text",
635
+ "email",
636
+ "password",
637
+ "number",
638
+ "tel",
639
+ "url",
640
+ "search",
641
+ "date",
642
+ "time",
643
+ "datetime-local",
644
+ "month",
645
+ "week",
646
+ "color",
647
+ "range",
648
+ "file",
649
+ "checkbox",
650
+ "radio"
651
+ ]);
652
+ var PERSONAL_DATA_FIELD_NAMES = /* @__PURE__ */ new Set([
653
+ "email",
654
+ "tel",
655
+ "name",
656
+ "given-name",
657
+ "family-name",
658
+ "additional-name",
659
+ "organization",
660
+ "organization-title",
661
+ "street-address",
662
+ "address-line1",
663
+ "address-line2",
664
+ "postal-code",
665
+ "country-name",
666
+ "bday",
667
+ "sex",
668
+ "url",
669
+ "username",
670
+ "new-password",
671
+ "current-password",
672
+ "one-time-code",
673
+ "cc-name",
674
+ "cc-number"
675
+ ]);
676
+ function isLabelableInput(node) {
677
+ const typeAttr = getAttr(node, "type");
678
+ if (!typeAttr) return true;
679
+ const typeVal = getAttrStringValue(typeAttr);
680
+ if (!typeVal) return true;
681
+ return LABELABLE_INPUT_TYPES.has(typeVal);
682
+ }
683
+ var formFieldsNeedLabel = defineRule(
684
+ { id: "forms/label-present", criteria: ["11.1"] },
685
+ (context) => {
686
+ const { filePath, ast } = context;
687
+ const issues = [];
688
+ walk(ast, {
689
+ JSXOpeningElement(rawNode) {
690
+ const node = rawNode;
691
+ const tag = getTagName(node);
692
+ if (!tag) return;
693
+ const isLabelable = tag === "textarea" || tag === "select" || tag === "input" && isLabelableInput(node);
694
+ if (!isLabelable) return;
695
+ const idAttr = getAttr(node, "id");
696
+ if (idAttr) return;
697
+ const ariaLabel = getAttr(node, "aria-label");
698
+ const ariaLabelledby = getAttr(node, "aria-labelledby");
699
+ const placeholder = getAttr(node, "placeholder");
700
+ if (isAttrDynamic(ariaLabel) || isAttrDynamic(ariaLabelledby)) return;
701
+ const hasStaticAriaLabel = ariaLabel && getAttrStringValue(ariaLabel)?.trim() !== "";
702
+ const hasAriaLabelledby = !!ariaLabelledby;
703
+ if (hasStaticAriaLabel || hasAriaLabelledby) return;
704
+ const severity = placeholder ? "warning" : "error";
705
+ const testId = tag === "textarea" ? "11.1.2" : tag === "select" ? "11.1.3" : "11.1.1";
706
+ issues.push(
707
+ createStaticIssue({
708
+ node,
709
+ filePath,
710
+ criterionId: "11.1",
711
+ testId,
712
+ severity,
713
+ messageKey: "form.missing-label",
714
+ remediationKey: "form.missing-label",
715
+ wcag: "1.3.1",
716
+ messageContext: { tag }
717
+ })
718
+ );
719
+ }
720
+ });
721
+ return issues;
722
+ }
723
+ );
724
+ var fieldsetNeedsLegend = defineRule(
725
+ { id: "forms/fieldset-legend", criteria: ["11.6"] },
726
+ (context) => {
727
+ const { filePath, ast } = context;
728
+ const issues = [];
729
+ walk(ast, {
730
+ JSXElement(rawNode) {
731
+ const node = rawNode;
732
+ const opening = node.openingElement;
733
+ const tag = getTagName(opening);
734
+ if (tag === "fieldset") {
735
+ const hasLegend = node.children.some(
736
+ (child) => child.type === "JSXElement" && getTagName(child.openingElement) === "legend"
737
+ );
738
+ if (!hasLegend) {
739
+ issues.push(
740
+ createStaticIssue({
741
+ node: opening,
742
+ filePath,
743
+ criterionId: "11.6",
744
+ testId: "11.6.1",
745
+ messageKey: "form.fieldset-missing-legend",
746
+ wcag: "1.3.1"
747
+ })
748
+ );
749
+ }
750
+ return;
751
+ }
752
+ const role = getAttrStringValue(getAttr(opening, "role"));
753
+ if (role === "group" || role === "radiogroup") {
754
+ const hasLabel = getAttr(opening, "aria-labelledby") || getAttr(opening, "aria-label");
755
+ if (!hasLabel) {
756
+ issues.push(
757
+ createStaticIssue({
758
+ node: opening,
759
+ filePath,
760
+ criterionId: "11.6",
761
+ testId: "11.6.2",
762
+ messageKey: "form.group-missing-label",
763
+ remediationKey: "form.fieldset-missing-legend",
764
+ wcag: "1.3.1",
765
+ messageContext: { role }
766
+ })
767
+ );
768
+ }
769
+ }
770
+ }
771
+ });
772
+ return issues;
773
+ }
774
+ );
775
+ var buttonsNeedLabel = defineRule({ id: "forms/button-label", criteria: ["11.9"] }, (context) => {
776
+ const { filePath, ast } = context;
777
+ const issues = [];
778
+ walk(ast, {
779
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: button/input label detection spans multiple element types
780
+ JSXElement(rawNode) {
781
+ const node = rawNode;
782
+ const opening = node.openingElement;
783
+ const tag = getTagName(opening);
784
+ if (tag === "button") {
785
+ const ariaLabel = getAttr(opening, "aria-label");
786
+ const ariaLabelledby = getAttr(opening, "aria-labelledby");
787
+ if (isAttrDynamic(ariaLabel) || isAttrDynamic(ariaLabelledby)) return;
788
+ const hasAriaLabel = ariaLabel && getAttrStringValue(ariaLabel)?.trim() !== "";
789
+ if (hasAriaLabel || ariaLabelledby) return;
790
+ const text = getTextContent(node);
791
+ if (text === null) return;
792
+ if (text.trim() === "") {
793
+ issues.push(
794
+ createStaticIssue({
795
+ node: opening,
796
+ filePath,
797
+ criterionId: "11.9",
798
+ testId: "11.9.1",
799
+ messageKey: "form.button-missing-label",
800
+ wcag: "1.3.1"
801
+ })
802
+ );
803
+ }
804
+ return;
805
+ }
806
+ if (tag === "input") {
807
+ const typeVal = getAttrStringValue(getAttr(opening, "type"));
808
+ if (typeVal !== "submit" && typeVal !== "button" && typeVal !== "reset") return;
809
+ const valueAttr = getAttr(opening, "value");
810
+ const ariaLabel = getAttr(opening, "aria-label");
811
+ if (ariaLabel || isAttrDynamic(valueAttr)) return;
812
+ const value = getAttrStringValue(valueAttr);
813
+ if (value === null || value.trim() !== "") return;
814
+ issues.push(
815
+ createStaticIssue({
816
+ node: opening,
817
+ filePath,
818
+ criterionId: "11.9",
819
+ testId: "11.9.2",
820
+ messageKey: "form.submit-empty-value",
821
+ remediationKey: "form.button-missing-label",
822
+ wcag: "1.3.1",
823
+ messageContext: { type: typeVal ?? "submit" }
824
+ })
825
+ );
826
+ }
827
+ }
828
+ });
829
+ return issues;
830
+ });
831
+ var PERSONAL_DATA_TYPES = /* @__PURE__ */ new Map([
832
+ ["email", "email"],
833
+ ["tel", "tel"]
834
+ ]);
835
+ var PERSONAL_DATA_NAME_PATTERNS = Array.from(
836
+ PERSONAL_DATA_FIELD_NAMES
837
+ ).map((name) => {
838
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/-/g, "[-_]");
839
+ return [new RegExp(`(?:^|[-_])${escaped}(?:$|[-_])`), name];
840
+ });
841
+ var autocompleteForPersonalData = defineRule(
842
+ { id: "forms/autocomplete-personal-data", criteria: ["11.13"] },
843
+ (context) => {
844
+ const { filePath, ast } = context;
845
+ const issues = [];
846
+ walk(ast, {
847
+ JSXOpeningElement(rawNode) {
848
+ const node = rawNode;
849
+ if (getTagName(node) !== "input") return;
850
+ const typeAttr = getAttr(node, "type");
851
+ const nameAttr = getAttr(node, "name");
852
+ const autocompleteAttr = getAttr(node, "autocomplete");
853
+ if (autocompleteAttr) return;
854
+ const typeVal = getAttrStringValue(typeAttr);
855
+ const nameVal = getAttrStringValue(nameAttr)?.toLowerCase() ?? "";
856
+ let purpose;
857
+ if (typeVal && PERSONAL_DATA_TYPES.has(typeVal)) {
858
+ purpose = PERSONAL_DATA_TYPES.get(typeVal);
859
+ } else {
860
+ for (const [re, token] of PERSONAL_DATA_NAME_PATTERNS) {
861
+ if (re.test(nameVal)) {
862
+ purpose = token;
863
+ break;
864
+ }
865
+ }
866
+ }
867
+ if (!purpose) return;
868
+ issues.push(
869
+ createStaticIssue({
870
+ node,
871
+ filePath,
872
+ criterionId: "11.13",
873
+ testId: "11.13.1",
874
+ severity: "warning",
875
+ messageKey: "form.missing-autocomplete",
876
+ wcag: "1.3.5",
877
+ messageContext: { purpose }
878
+ })
879
+ );
880
+ }
881
+ });
882
+ return issues;
883
+ }
884
+ );
885
+ var formRules = [
886
+ formFieldsNeedLabel,
887
+ fieldsetNeedsLegend,
888
+ buttonsNeedLabel,
889
+ autocompleteForPersonalData
890
+ ];
891
+
892
+ // src/analyzer/static/rules/index.ts
893
+ var ALL_STATIC_RULES = [
894
+ ...imageRules,
895
+ ...frameRules,
896
+ ...tableRules,
897
+ ...linkRules,
898
+ ...mandatoryRules,
899
+ ...structureRules,
900
+ ...formRules
901
+ ];
902
+
903
+ // src/index.ts
904
+ function defineConfig(config) {
905
+ return config;
906
+ }
907
+ export {
908
+ ALL_CRITERIA,
909
+ ALL_STATIC_RULES,
910
+ AXE_TEST_ID_PREFIX,
911
+ AXE_TO_RGAA,
912
+ AnalysisError,
913
+ CRITERIA_BY_ID,
914
+ ConfigError,
915
+ KodaRGAAConfigSchema,
916
+ THEMES,
917
+ TOTAL_CRITERIA,
918
+ TimeoutError,
919
+ analyze,
920
+ buildReport,
921
+ defineConfig,
922
+ generateDefaultConfig,
923
+ getAxeRulesForCriterion,
924
+ getRGAACriteria,
925
+ getSupportedLocales,
926
+ getTranslations,
927
+ interpolate,
928
+ loadConfig,
929
+ loadTranslations,
930
+ printReport,
931
+ resolveConfigPath,
932
+ runRuntimeAnalysis,
933
+ runStaticAnalysis,
934
+ writeHtmlReport,
935
+ writeJsonReport,
936
+ writeJunitReport,
937
+ writeMarkdownReport,
938
+ writeReports,
939
+ writeSarifReport
940
+ };
941
+ //# sourceMappingURL=index.js.map