@svelte-vitals/core 0.42.0 → 0.43.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/README.md +1 -1
- package/dist/index.d.ts +208 -6
- package/dist/index.js +922 -54
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,14 @@ var defaultProject = {
|
|
|
4
4
|
hasSitemap: false,
|
|
5
5
|
htmlLang: { presence: "none", value: "absent" }
|
|
6
6
|
};
|
|
7
|
-
var CATEGORIES = [
|
|
7
|
+
var CATEGORIES = [
|
|
8
|
+
"seo",
|
|
9
|
+
"performance",
|
|
10
|
+
"correctness",
|
|
11
|
+
"security",
|
|
12
|
+
"architecture",
|
|
13
|
+
"a11y"
|
|
14
|
+
];
|
|
8
15
|
var defaultConfig = {
|
|
9
16
|
treatDynamicAs: "pass",
|
|
10
17
|
metaComponents: [],
|
|
@@ -15,6 +22,71 @@ function defineConfig(config = {}) {
|
|
|
15
22
|
return { ...defaultConfig, ...config };
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
// src/a11y.ts
|
|
26
|
+
function foldOccurrences(nodes) {
|
|
27
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
if (node.repeatable) continue;
|
|
30
|
+
const list = byKey.get(node.key);
|
|
31
|
+
if (list) list.push(node);
|
|
32
|
+
else byKey.set(node.key, [node]);
|
|
33
|
+
}
|
|
34
|
+
const folded = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const [key, list] of byKey) folded.set(key, foldAt(list, 0));
|
|
36
|
+
return folded;
|
|
37
|
+
}
|
|
38
|
+
function foldAt(nodes, depth) {
|
|
39
|
+
const unconditional = [];
|
|
40
|
+
const groups = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const node of nodes) {
|
|
42
|
+
const step = node.path[depth];
|
|
43
|
+
if (!step) {
|
|
44
|
+
unconditional.push(node);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let branches = groups.get(step.group);
|
|
48
|
+
if (!branches) groups.set(step.group, branches = /* @__PURE__ */ new Map());
|
|
49
|
+
const list = branches.get(step.branch);
|
|
50
|
+
if (list) list.push(node);
|
|
51
|
+
else branches.set(step.branch, [node]);
|
|
52
|
+
}
|
|
53
|
+
const representatives = [...unconditional];
|
|
54
|
+
for (const branches of groups.values()) {
|
|
55
|
+
let best = [];
|
|
56
|
+
let bestBranch = Number.POSITIVE_INFINITY;
|
|
57
|
+
for (const [branch, list] of branches) {
|
|
58
|
+
const arm = foldAt(list, depth + 1);
|
|
59
|
+
if (arm.length > best.length || arm.length === best.length && branch < bestBranch) {
|
|
60
|
+
best = arm;
|
|
61
|
+
bestBranch = branch;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
representatives.push(...best);
|
|
65
|
+
}
|
|
66
|
+
return representatives;
|
|
67
|
+
}
|
|
68
|
+
function decodeFragmentId(fragment) {
|
|
69
|
+
try {
|
|
70
|
+
return decodeURIComponent(fragment);
|
|
71
|
+
} catch {
|
|
72
|
+
return fragment;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function splitTokens(value) {
|
|
76
|
+
return value ? value.trim().split(/\s+/).filter(Boolean) : [];
|
|
77
|
+
}
|
|
78
|
+
var LANDMARK_ROLES = /* @__PURE__ */ new Set(["main", "banner", "contentinfo", "complementary"]);
|
|
79
|
+
var IDREF_ATTRS = [
|
|
80
|
+
"for",
|
|
81
|
+
"aria-labelledby",
|
|
82
|
+
"aria-describedby",
|
|
83
|
+
"aria-controls",
|
|
84
|
+
"aria-activedescendant"
|
|
85
|
+
];
|
|
86
|
+
function isTopFragment(id) {
|
|
87
|
+
return id.toLowerCase() === "top";
|
|
88
|
+
}
|
|
89
|
+
|
|
18
90
|
// src/component-parse.ts
|
|
19
91
|
import { parse } from "svelte/compiler";
|
|
20
92
|
|
|
@@ -82,6 +154,57 @@ function attrTextOf(attr) {
|
|
|
82
154
|
return Array.isArray(v) ? textFromNodes(v) : void 0;
|
|
83
155
|
}
|
|
84
156
|
|
|
157
|
+
// src/rules/a11y/interactive.ts
|
|
158
|
+
var ALWAYS_INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary", "embed", "iframe"]);
|
|
159
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
160
|
+
"button",
|
|
161
|
+
"link",
|
|
162
|
+
"checkbox",
|
|
163
|
+
"radio",
|
|
164
|
+
"switch",
|
|
165
|
+
"tab",
|
|
166
|
+
"menuitem",
|
|
167
|
+
"menuitemcheckbox",
|
|
168
|
+
"menuitemradio",
|
|
169
|
+
"option",
|
|
170
|
+
"slider",
|
|
171
|
+
"spinbutton",
|
|
172
|
+
"textbox",
|
|
173
|
+
"combobox",
|
|
174
|
+
"searchbox",
|
|
175
|
+
"scrollbar",
|
|
176
|
+
"gridcell"
|
|
177
|
+
]);
|
|
178
|
+
function literalOf(attrs, name) {
|
|
179
|
+
return attrs.find((a) => a.name === name)?.literal;
|
|
180
|
+
}
|
|
181
|
+
function hasInteractiveRole(attrs) {
|
|
182
|
+
const role = splitTokens(literalOf(attrs, "role"))[0];
|
|
183
|
+
return role !== void 0 && INTERACTIVE_ROLES.has(role);
|
|
184
|
+
}
|
|
185
|
+
function isInteractiveElement(tag, attrs) {
|
|
186
|
+
if (ALWAYS_INTERACTIVE_TAGS.has(tag)) return true;
|
|
187
|
+
if (tag === "a" && literalOf(attrs, "href") !== void 0) return true;
|
|
188
|
+
if (tag === "input") {
|
|
189
|
+
const typeAttr = attrs.find((a) => a.name === "type");
|
|
190
|
+
if (!typeAttr) return true;
|
|
191
|
+
if (typeAttr.literal !== void 0) return typeAttr.literal.toLowerCase() !== "hidden";
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if ((tag === "audio" || tag === "video") && attrs.some((a) => a.name === "controls")) return true;
|
|
195
|
+
const tabindex = literalOf(attrs, "tabindex")?.trim();
|
|
196
|
+
if (tabindex) {
|
|
197
|
+
const n = Number(tabindex);
|
|
198
|
+
if (Number.isFinite(n) && n >= 0) return true;
|
|
199
|
+
}
|
|
200
|
+
return hasInteractiveRole(attrs);
|
|
201
|
+
}
|
|
202
|
+
function isInteractiveContainer(tag, attrs) {
|
|
203
|
+
if (tag === "button") return true;
|
|
204
|
+
if (tag === "a") return literalOf(attrs, "href") !== void 0;
|
|
205
|
+
return hasInteractiveRole(attrs);
|
|
206
|
+
}
|
|
207
|
+
|
|
85
208
|
// src/component-parse.ts
|
|
86
209
|
function unwrapTs(expr) {
|
|
87
210
|
let cur = expr;
|
|
@@ -715,6 +838,304 @@ function collectCheckableBindValues(node, source, acc) {
|
|
|
715
838
|
if (key in node) collectCheckableBindValues(node[key], source, acc);
|
|
716
839
|
}
|
|
717
840
|
}
|
|
841
|
+
function classifyAttrValue(value) {
|
|
842
|
+
if (value === true) return { literal: "" };
|
|
843
|
+
if (Array.isArray(value) && value.length === 1 && value[0]?.type === "Text") {
|
|
844
|
+
return { literal: String(value[0].data ?? "") };
|
|
845
|
+
}
|
|
846
|
+
return { expression: true };
|
|
847
|
+
}
|
|
848
|
+
function collectAriaElements(node, source, acc) {
|
|
849
|
+
if (Array.isArray(node)) {
|
|
850
|
+
for (const child of node) collectAriaElements(child, source, acc);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!node || typeof node !== "object") return;
|
|
854
|
+
if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
|
|
855
|
+
const roleAttr = findAttr(node.attributes, "role");
|
|
856
|
+
const ariaAttrs = node.attributes.filter(
|
|
857
|
+
(a) => a?.type === "Attribute" && typeof a.name === "string" && a.name.startsWith("aria-")
|
|
858
|
+
);
|
|
859
|
+
if (roleAttr || ariaAttrs.length > 0) {
|
|
860
|
+
const inputType = node.name === "input" ? attrText(node.attributes, "type") : void 0;
|
|
861
|
+
const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
|
|
862
|
+
acc.push({
|
|
863
|
+
tag: node.name,
|
|
864
|
+
line: lineOf(source, node.start),
|
|
865
|
+
...roleAttr ? { role: classifyAttrValue(roleAttr.value) } : {},
|
|
866
|
+
...inputType !== void 0 ? { inputType: inputType.toLowerCase() } : {},
|
|
867
|
+
...hasSpread ? { hasSpread: true } : {},
|
|
868
|
+
aria: ariaAttrs.map((a) => ({
|
|
869
|
+
name: a.name,
|
|
870
|
+
line: lineOf(source, a.start ?? node.start),
|
|
871
|
+
...classifyAttrValue(a.value)
|
|
872
|
+
}))
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
877
|
+
if (key in node) collectAriaElements(node[key], source, acc);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function elementAttrs(attributes) {
|
|
881
|
+
return attributes.filter((a) => a?.type === "Attribute" && typeof a.name === "string").map((a) => ({ name: a.name, ...classifyAttrValue(a.value) }));
|
|
882
|
+
}
|
|
883
|
+
function collectInteractiveNestings(node, source, acc, stack) {
|
|
884
|
+
if (Array.isArray(node)) {
|
|
885
|
+
for (const child of node) collectInteractiveNestings(child, source, acc, stack);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (!node || typeof node !== "object") return;
|
|
889
|
+
if (node.type === "SnippetBlock") {
|
|
890
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
891
|
+
if (key in node) collectInteractiveNestings(node[key], source, acc, []);
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
let opened = false;
|
|
896
|
+
if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
|
|
897
|
+
const attrs = elementAttrs(node.attributes);
|
|
898
|
+
if (stack.length > 0 && isInteractiveElement(node.name, attrs)) {
|
|
899
|
+
acc.push({
|
|
900
|
+
containerTag: stack[stack.length - 1],
|
|
901
|
+
descendantTag: node.name,
|
|
902
|
+
line: lineOf(source, node.start)
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (isInteractiveContainer(node.name, attrs)) {
|
|
906
|
+
stack.push(node.name);
|
|
907
|
+
opened = true;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
911
|
+
if (key in node) collectInteractiveNestings(node[key], source, acc, stack);
|
|
912
|
+
}
|
|
913
|
+
if (opened) stack.pop();
|
|
914
|
+
}
|
|
915
|
+
function hasNamingAttr(attributes) {
|
|
916
|
+
return ["aria-label", "aria-labelledby", "title"].some((name) => {
|
|
917
|
+
const attr = findAttr(attributes, name);
|
|
918
|
+
if (!attr) return false;
|
|
919
|
+
const v = classifyAttrValue(attr.value);
|
|
920
|
+
return "expression" in v || v.literal !== void 0 && v.literal.trim().length > 0;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function scanAccessibleNameSubtree(node) {
|
|
924
|
+
if (Array.isArray(node)) {
|
|
925
|
+
const acc2 = { named: false, unknowable: false };
|
|
926
|
+
for (const child of node) {
|
|
927
|
+
const r = scanAccessibleNameSubtree(child);
|
|
928
|
+
acc2.named ||= r.named;
|
|
929
|
+
acc2.unknowable ||= r.unknowable;
|
|
930
|
+
}
|
|
931
|
+
return acc2;
|
|
932
|
+
}
|
|
933
|
+
if (!node || typeof node !== "object") return { named: false, unknowable: false };
|
|
934
|
+
if (node.type === "SnippetBlock") return { named: false, unknowable: false };
|
|
935
|
+
if (node.type === "Text") return { named: String(node.data ?? "").trim().length > 0, unknowable: false };
|
|
936
|
+
if (node.type === "ExpressionTag" || node.type === "RenderTag" || node.type === "HtmlTag" || COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
937
|
+
return { named: false, unknowable: true };
|
|
938
|
+
}
|
|
939
|
+
if (node.type === "RegularElement" && node.name === "img" && Array.isArray(node.attributes)) {
|
|
940
|
+
const alt = attrText(node.attributes, "alt");
|
|
941
|
+
if (alt !== void 0 && alt.trim().length > 0) return { named: true, unknowable: false };
|
|
942
|
+
}
|
|
943
|
+
const acc = { named: false, unknowable: false };
|
|
944
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
945
|
+
if (key in node) {
|
|
946
|
+
const r = scanAccessibleNameSubtree(node[key]);
|
|
947
|
+
acc.named ||= r.named;
|
|
948
|
+
acc.unknowable ||= r.unknowable;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return acc;
|
|
952
|
+
}
|
|
953
|
+
function accessibleNameTarget(node) {
|
|
954
|
+
if (node.name === "button") return "button";
|
|
955
|
+
if (node.name === "a") return attrText(node.attributes, "href") !== void 0 ? "a" : void 0;
|
|
956
|
+
if (node.name === "input") {
|
|
957
|
+
const type = attrText(node.attributes, "type");
|
|
958
|
+
return type?.toLowerCase() === "image" ? "input" : void 0;
|
|
959
|
+
}
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
function collectUnnamedInteractive(node, source, acc) {
|
|
963
|
+
if (Array.isArray(node)) {
|
|
964
|
+
for (const child of node) collectUnnamedInteractive(child, source, acc);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!node || typeof node !== "object") return;
|
|
968
|
+
if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
|
|
969
|
+
const target = accessibleNameTarget(node);
|
|
970
|
+
const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
|
|
971
|
+
if (target && !hasSpread) {
|
|
972
|
+
if (target === "input") {
|
|
973
|
+
const alt = attrText(node.attributes, "alt");
|
|
974
|
+
if (!hasNamingAttr(node.attributes) && !(alt !== void 0 && alt.trim().length > 0)) {
|
|
975
|
+
acc.push({ tag: node.name, line: lineOf(source, node.start) });
|
|
976
|
+
}
|
|
977
|
+
} else {
|
|
978
|
+
const scan = scanAccessibleNameSubtree(node);
|
|
979
|
+
if (!hasNamingAttr(node.attributes) && !scan.named && !scan.unknowable) {
|
|
980
|
+
acc.push({ tag: node.name, line: lineOf(source, node.start) });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
986
|
+
if (key in node) collectUnnamedInteractive(node[key], source, acc);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
var LABELABLE_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button", "meter", "output", "progress"]);
|
|
990
|
+
function isLabelableDescendant(node) {
|
|
991
|
+
if (node.type !== "RegularElement" || !LABELABLE_TAGS.has(node.name)) return false;
|
|
992
|
+
if (node.name !== "input") return true;
|
|
993
|
+
return attrText(node.attributes ?? [], "type")?.toLowerCase() !== "hidden";
|
|
994
|
+
}
|
|
995
|
+
function scanLabelSubtree(node) {
|
|
996
|
+
if (Array.isArray(node)) {
|
|
997
|
+
const acc2 = { hasControl: false, unknowable: false };
|
|
998
|
+
for (const child of node) {
|
|
999
|
+
const r = scanLabelSubtree(child);
|
|
1000
|
+
acc2.hasControl ||= r.hasControl;
|
|
1001
|
+
acc2.unknowable ||= r.unknowable;
|
|
1002
|
+
}
|
|
1003
|
+
return acc2;
|
|
1004
|
+
}
|
|
1005
|
+
if (!node || typeof node !== "object") return { hasControl: false, unknowable: false };
|
|
1006
|
+
if (node.type === "SnippetBlock") return { hasControl: false, unknowable: false };
|
|
1007
|
+
if (node.type === "ExpressionTag" || node.type === "RenderTag" || node.type === "HtmlTag" || COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
1008
|
+
return { hasControl: false, unknowable: true };
|
|
1009
|
+
}
|
|
1010
|
+
if (isLabelableDescendant(node)) return { hasControl: true, unknowable: false };
|
|
1011
|
+
const acc = { hasControl: false, unknowable: false };
|
|
1012
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
1013
|
+
if (key in node) {
|
|
1014
|
+
const r = scanLabelSubtree(node[key]);
|
|
1015
|
+
acc.hasControl ||= r.hasControl;
|
|
1016
|
+
acc.unknowable ||= r.unknowable;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return acc;
|
|
1020
|
+
}
|
|
1021
|
+
function collectUnassociatedLabels(node, source, acc) {
|
|
1022
|
+
if (Array.isArray(node)) {
|
|
1023
|
+
for (const child of node) collectUnassociatedLabels(child, source, acc);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (!node || typeof node !== "object") return;
|
|
1027
|
+
if (node.type === "RegularElement" && node.name === "label" && Array.isArray(node.attributes)) {
|
|
1028
|
+
const hasFor = findAttr(node.attributes, "for") !== void 0;
|
|
1029
|
+
const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
|
|
1030
|
+
if (!hasFor && !hasSpread) {
|
|
1031
|
+
const scan = scanLabelSubtree(node);
|
|
1032
|
+
if (!scan.hasControl && !scan.unknowable) {
|
|
1033
|
+
acc.push({ line: lineOf(source, node.start) });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
1038
|
+
if (key in node) collectUnassociatedLabels(node[key], source, acc);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
var BULLET_TEXT_RE = /^[•・·\-*]\s/;
|
|
1042
|
+
function collectBulletTexts(node, source, acc, insideLi) {
|
|
1043
|
+
if (Array.isArray(node)) {
|
|
1044
|
+
for (const child of node) collectBulletTexts(child, source, acc, insideLi);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (!node || typeof node !== "object") return;
|
|
1048
|
+
if (node.type === "Text") {
|
|
1049
|
+
const trimmed = String(node.data ?? "").trim();
|
|
1050
|
+
if (!insideLi && BULLET_TEXT_RE.test(trimmed)) {
|
|
1051
|
+
acc.push({ line: lineOf(source, node.start), char: trimmed[0] });
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (node.type === "SnippetBlock") return;
|
|
1056
|
+
const nowInsideLi = insideLi || node.type === "RegularElement" && node.name === "li";
|
|
1057
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
1058
|
+
if (key in node) collectBulletTexts(node[key], source, acc, nowInsideLi);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function selectNeedsPlaceholder(attributes) {
|
|
1062
|
+
const requiredAttr = findAttr(attributes, "required");
|
|
1063
|
+
if (!requiredAttr || attrValueOf(requiredAttr) === "dynamic") return false;
|
|
1064
|
+
if (findAttr(attributes, "multiple")) return false;
|
|
1065
|
+
const sizeAttr = findAttr(attributes, "size");
|
|
1066
|
+
if (sizeAttr) {
|
|
1067
|
+
if (attrValueOf(sizeAttr) === "dynamic") return false;
|
|
1068
|
+
const size = Number(attrText(attributes, "size"));
|
|
1069
|
+
if (!(Number.isFinite(size) && size <= 1)) return false;
|
|
1070
|
+
}
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
function firstSignificantChild(nodes) {
|
|
1074
|
+
for (const child of nodes ?? []) {
|
|
1075
|
+
if (!child) continue;
|
|
1076
|
+
if (child.type === "Comment") continue;
|
|
1077
|
+
if (child.type === "Text" && !String(child.data ?? "").trim()) continue;
|
|
1078
|
+
return child;
|
|
1079
|
+
}
|
|
1080
|
+
return void 0;
|
|
1081
|
+
}
|
|
1082
|
+
function isPlaceholderOption(option) {
|
|
1083
|
+
const attributes = option.attributes ?? [];
|
|
1084
|
+
if (findAttr(attributes, "value")) {
|
|
1085
|
+
const literal = attrText(attributes, "value");
|
|
1086
|
+
return literal === void 0 || literal === "";
|
|
1087
|
+
}
|
|
1088
|
+
return textFromNodes(option.fragment?.nodes ?? []) === void 0;
|
|
1089
|
+
}
|
|
1090
|
+
function collectSelectsMissingPlaceholder(node, source, acc) {
|
|
1091
|
+
if (Array.isArray(node)) {
|
|
1092
|
+
for (const child of node) collectSelectsMissingPlaceholder(child, source, acc);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (!node || typeof node !== "object") return;
|
|
1096
|
+
if (node.type === "RegularElement" && node.name === "select" && Array.isArray(node.attributes)) {
|
|
1097
|
+
const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
|
|
1098
|
+
if (!hasSpread && selectNeedsPlaceholder(node.attributes)) {
|
|
1099
|
+
const first = firstSignificantChild(node.fragment?.nodes);
|
|
1100
|
+
if (!first) {
|
|
1101
|
+
acc.push({ line: lineOf(source, node.start) });
|
|
1102
|
+
} else if (first.type === "RegularElement" && first.name === "option") {
|
|
1103
|
+
const firstHasSpread = (first.attributes ?? []).some((a) => a?.type === "SpreadAttribute");
|
|
1104
|
+
if (!firstHasSpread && !isPlaceholderOption(first)) acc.push({ line: lineOf(source, node.start) });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
1109
|
+
if (key in node) collectSelectsMissingPlaceholder(node[key], source, acc);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
var MACHINE_READABLE_TIME = [
|
|
1113
|
+
/^\d{4}(-\d{2}){0,2}$/,
|
|
1114
|
+
/^\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/,
|
|
1115
|
+
/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/,
|
|
1116
|
+
/^\d{2}-\d{2}$/,
|
|
1117
|
+
/^P(?=\d|T)/i
|
|
1118
|
+
];
|
|
1119
|
+
function collectTimesMissingDatetime(node, source, acc) {
|
|
1120
|
+
if (Array.isArray(node)) {
|
|
1121
|
+
for (const child of node) collectTimesMissingDatetime(child, source, acc);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!node || typeof node !== "object") return;
|
|
1125
|
+
if (node.type === "RegularElement" && node.name === "time" && findAttr(node.attributes ?? [], "datetime") === void 0 && !(node.attributes ?? []).some((a) => a?.type === "SpreadAttribute")) {
|
|
1126
|
+
const nodes = node.fragment?.nodes ?? [];
|
|
1127
|
+
if (nodes.length > 0 && nodes.every((n) => n?.type === "Text")) {
|
|
1128
|
+
const text = nodes.map((n) => String(n.data ?? "")).join("");
|
|
1129
|
+
const trimmed = text.trim();
|
|
1130
|
+
if (trimmed.length > 0 && !MACHINE_READABLE_TIME.some((re) => re.test(trimmed))) {
|
|
1131
|
+
acc.push({ line: lineOf(source, node.start), text: trimmed });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
1136
|
+
if (key in node) collectTimesMissingDatetime(node[key], source, acc);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
718
1139
|
function collectHrefLinks(node, source, acc) {
|
|
719
1140
|
if (Array.isArray(node)) {
|
|
720
1141
|
for (const child of node) collectHrefLinks(child, source, acc);
|
|
@@ -1336,6 +1757,20 @@ function parseComponentFacts(source, filename) {
|
|
|
1336
1757
|
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
1337
1758
|
const checkableBindValues = [];
|
|
1338
1759
|
collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
|
|
1760
|
+
const ariaElements = [];
|
|
1761
|
+
collectAriaElements(ast.fragment ?? ast, source, ariaElements);
|
|
1762
|
+
const interactiveNestings = [];
|
|
1763
|
+
collectInteractiveNestings(ast.fragment ?? ast, source, interactiveNestings, []);
|
|
1764
|
+
const unnamedInteractive = [];
|
|
1765
|
+
collectUnnamedInteractive(ast.fragment ?? ast, source, unnamedInteractive);
|
|
1766
|
+
const unassociatedLabels = [];
|
|
1767
|
+
collectUnassociatedLabels(ast.fragment ?? ast, source, unassociatedLabels);
|
|
1768
|
+
const bulletTexts = [];
|
|
1769
|
+
collectBulletTexts(ast.fragment ?? ast, source, bulletTexts, false);
|
|
1770
|
+
const selectsMissingPlaceholder = [];
|
|
1771
|
+
collectSelectsMissingPlaceholder(ast.fragment ?? ast, source, selectsMissingPlaceholder);
|
|
1772
|
+
const timesMissingDatetime = [];
|
|
1773
|
+
collectTimesMissingDatetime(ast.fragment ?? ast, source, timesMissingDatetime);
|
|
1339
1774
|
const basePathLinks = [];
|
|
1340
1775
|
collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
|
|
1341
1776
|
const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
|
|
@@ -1538,7 +1973,14 @@ function parseComponentFacts(source, filename) {
|
|
|
1538
1973
|
browserGlobalRefs,
|
|
1539
1974
|
moduleStateDecls: [],
|
|
1540
1975
|
suppressions,
|
|
1541
|
-
commentLinks: collectCommentLinks(source)
|
|
1976
|
+
commentLinks: collectCommentLinks(source),
|
|
1977
|
+
ariaElements,
|
|
1978
|
+
interactiveNestings,
|
|
1979
|
+
unnamedInteractive,
|
|
1980
|
+
unassociatedLabels,
|
|
1981
|
+
bulletTexts,
|
|
1982
|
+
selectsMissingPlaceholder,
|
|
1983
|
+
timesMissingDatetime
|
|
1542
1984
|
};
|
|
1543
1985
|
}
|
|
1544
1986
|
|
|
@@ -2408,10 +2850,10 @@ function findKitPathsBaseInViteConfig(source) {
|
|
|
2408
2850
|
}
|
|
2409
2851
|
function resolveKitPathsBase(viteConfig, svelteConfig) {
|
|
2410
2852
|
if (viteConfig) {
|
|
2411
|
-
const
|
|
2412
|
-
if (
|
|
2413
|
-
if (
|
|
2414
|
-
return
|
|
2853
|
+
const result3 = findKitPathsBaseInViteConfig(viteConfig.source);
|
|
2854
|
+
if (result3.kind === "unresolvable") return void 0;
|
|
2855
|
+
if (result3.kind === "resolved") {
|
|
2856
|
+
return result3.base ? { ...result3.base, file: viteConfig.file } : void 0;
|
|
2415
2857
|
}
|
|
2416
2858
|
}
|
|
2417
2859
|
if (!svelteConfig) return void 0;
|
|
@@ -3047,9 +3489,9 @@ function formatFailedRuleWarning(f) {
|
|
|
3047
3489
|
return `rule ${f.id} failed and was skipped: ${f.message.split("\n")[0]}`;
|
|
3048
3490
|
}
|
|
3049
3491
|
function applyRuleSeverities(results, config) {
|
|
3050
|
-
return results.map((
|
|
3051
|
-
const severity = settingSeverity(config.rules[
|
|
3052
|
-
return severity !== void 0 && severity !== "off" ? { ...
|
|
3492
|
+
return results.map((result3) => {
|
|
3493
|
+
const severity = settingSeverity(config.rules[result3.id]);
|
|
3494
|
+
return severity !== void 0 && severity !== "off" ? { ...result3, severity } : result3;
|
|
3053
3495
|
});
|
|
3054
3496
|
}
|
|
3055
3497
|
function routeGlobToRegExp(pattern) {
|
|
@@ -3076,15 +3518,15 @@ function applyOverrides(results, config) {
|
|
|
3076
3518
|
const compiled = compileOverrides(config);
|
|
3077
3519
|
if (compiled.length === 0) return results;
|
|
3078
3520
|
const out = [];
|
|
3079
|
-
for (const
|
|
3521
|
+
for (const result3 of results) {
|
|
3080
3522
|
let severity;
|
|
3081
3523
|
for (const o of compiled) {
|
|
3082
|
-
if (!overrideMatches(o, { route:
|
|
3083
|
-
const sev = settingSeverity(o.rules[
|
|
3524
|
+
if (!overrideMatches(o, { route: result3.route, file: result3.location })) continue;
|
|
3525
|
+
const sev = settingSeverity(o.rules[result3.id]) ?? settingSeverity(o.rules[result3.category ?? "seo"]);
|
|
3084
3526
|
if (sev !== void 0) severity = sev;
|
|
3085
3527
|
}
|
|
3086
|
-
if (severity === void 0) out.push(
|
|
3087
|
-
else if (severity !== "off") out.push({ ...
|
|
3528
|
+
if (severity === void 0) out.push(result3);
|
|
3529
|
+
else if (severity !== "off") out.push({ ...result3, severity });
|
|
3088
3530
|
}
|
|
3089
3531
|
return out;
|
|
3090
3532
|
}
|
|
@@ -3922,7 +4364,7 @@ function lengthRule(opts) {
|
|
|
3922
4364
|
const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
|
|
3923
4365
|
const min = intOption(o, "min", opts.min);
|
|
3924
4366
|
const max = intOption(o, "max", opts.max);
|
|
3925
|
-
const
|
|
4367
|
+
const recommendation12 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
3926
4368
|
const len = visibleLength(tag.text);
|
|
3927
4369
|
let problem;
|
|
3928
4370
|
if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
|
|
@@ -3936,7 +4378,7 @@ function lengthRule(opts) {
|
|
|
3936
4378
|
route: head.route,
|
|
3937
4379
|
location,
|
|
3938
4380
|
message: problem,
|
|
3939
|
-
recommendation:
|
|
4381
|
+
recommendation: recommendation12,
|
|
3940
4382
|
docsUrl: docsUrl12
|
|
3941
4383
|
} : {
|
|
3942
4384
|
id: opts.id,
|
|
@@ -3950,7 +4392,7 @@ function lengthRule(opts) {
|
|
|
3950
4392
|
// it to also apply `severity: 'off'`.
|
|
3951
4393
|
location,
|
|
3952
4394
|
message: opts.label,
|
|
3953
|
-
recommendation:
|
|
4395
|
+
recommendation: recommendation12,
|
|
3954
4396
|
docsUrl: docsUrl12
|
|
3955
4397
|
}
|
|
3956
4398
|
);
|
|
@@ -4304,7 +4746,7 @@ function fileRule(spec) {
|
|
|
4304
4746
|
for (const f of spec.facts(ctx) ?? []) {
|
|
4305
4747
|
const o = resolveRuleOptions(spec.id, spec.options, ctx.config, { route: f.file, file: f.file }, compiled);
|
|
4306
4748
|
if (!spec.applies(f, o, ctx)) continue;
|
|
4307
|
-
const
|
|
4749
|
+
const recommendation12 = typeof spec.recommendation === "function" ? spec.recommendation(o) : spec.recommendation;
|
|
4308
4750
|
const bad = spec.bad(f, o, ctx).filter((b) => !(b.line > 0 && isSuppressed(f.suppressions, spec.id, b.line)));
|
|
4309
4751
|
if (bad.length === 0) {
|
|
4310
4752
|
out.push({
|
|
@@ -4315,7 +4757,7 @@ function fileRule(spec) {
|
|
|
4315
4757
|
route: f.file,
|
|
4316
4758
|
location: f.file,
|
|
4317
4759
|
message: spec.label,
|
|
4318
|
-
recommendation:
|
|
4760
|
+
recommendation: recommendation12,
|
|
4319
4761
|
docsUrl: docsUrl12
|
|
4320
4762
|
});
|
|
4321
4763
|
continue;
|
|
@@ -4330,7 +4772,7 @@ function fileRule(spec) {
|
|
|
4330
4772
|
location: f.file,
|
|
4331
4773
|
...b.line > 0 ? { line: b.line } : {},
|
|
4332
4774
|
message: b.message,
|
|
4333
|
-
recommendation:
|
|
4775
|
+
recommendation: recommendation12,
|
|
4334
4776
|
docsUrl: docsUrl12,
|
|
4335
4777
|
...spec.fix ? { fix: { ...spec.fix } } : {}
|
|
4336
4778
|
});
|
|
@@ -5704,7 +6146,7 @@ var architectureReservedNamePlacement = {
|
|
|
5704
6146
|
const inCapUnits = Object.hasOwn(capUnits, name);
|
|
5705
6147
|
const inAnyUnits = Object.hasOwn(anyUnits, name);
|
|
5706
6148
|
if (!inPlacements && !inCapUnits && !inAnyUnits) continue;
|
|
5707
|
-
const emptyValue = (
|
|
6149
|
+
const emptyValue = (present4, value) => present4 && globsOf(value ?? "").length === 0;
|
|
5708
6150
|
if (emptyValue(inPlacements, placements[name]) || emptyValue(inCapUnits, capUnits[name]) || emptyValue(inAnyUnits, anyUnits[name])) {
|
|
5709
6151
|
continue;
|
|
5710
6152
|
}
|
|
@@ -5821,9 +6263,9 @@ var routeEntryImportsCache = /* @__PURE__ */ new WeakMap();
|
|
|
5821
6263
|
function cachedRouteEntryImports(c, ctx) {
|
|
5822
6264
|
const cached = routeEntryImportsCache.get(c);
|
|
5823
6265
|
if (cached !== void 0 && cached.aliases === ctx.project.kitAliases) return cached.result;
|
|
5824
|
-
const
|
|
5825
|
-
routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result });
|
|
5826
|
-
return
|
|
6266
|
+
const result3 = routeEntryImports(c, ctx);
|
|
6267
|
+
routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result: result3 });
|
|
6268
|
+
return result3;
|
|
5827
6269
|
}
|
|
5828
6270
|
var architectureRouteComponentImport = componentRule({
|
|
5829
6271
|
id: ID8,
|
|
@@ -6052,6 +6494,386 @@ var performanceStateRaw = componentRule({
|
|
|
6052
6494
|
}))
|
|
6053
6495
|
});
|
|
6054
6496
|
|
|
6497
|
+
// src/rules/a11y/aria-data.ts
|
|
6498
|
+
import { roles, aria } from "aria-query";
|
|
6499
|
+
function isKnownRole(role) {
|
|
6500
|
+
return roles.has(role);
|
|
6501
|
+
}
|
|
6502
|
+
function isAbstractRole(role) {
|
|
6503
|
+
return roles.get(role)?.abstract === true;
|
|
6504
|
+
}
|
|
6505
|
+
function isKnownAriaAttribute(name) {
|
|
6506
|
+
return aria.has(name);
|
|
6507
|
+
}
|
|
6508
|
+
function requiredAriaProps(role) {
|
|
6509
|
+
const def = roles.get(role);
|
|
6510
|
+
return def ? Object.keys(def.requiredProps) : [];
|
|
6511
|
+
}
|
|
6512
|
+
function ariaValueKind(name) {
|
|
6513
|
+
const def = aria.get(name);
|
|
6514
|
+
if (!def) return void 0;
|
|
6515
|
+
return { type: def.type, ...def.values ? { values: def.values.map(String) } : {} };
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6518
|
+
// src/rules/a11y/invalid-role.ts
|
|
6519
|
+
var a11yInvalidRole = componentRule({
|
|
6520
|
+
id: "a11y/invalid-role",
|
|
6521
|
+
title: "Invalid ARIA role",
|
|
6522
|
+
category: "a11y",
|
|
6523
|
+
label: "ARIA roles",
|
|
6524
|
+
rationale: "A role that does not exist in WAI-ARIA (or is abstract, reserved for the spec itself) is ignored or misread by assistive technology, silently breaking the element\u2019s announced semantics.",
|
|
6525
|
+
recommendation: "Use a concrete WAI-ARIA role; abstract roles and typos are ignored by assistive technology.",
|
|
6526
|
+
applies: (c) => (c.ariaElements ?? []).some((e) => e.role?.literal !== void 0),
|
|
6527
|
+
bad: (c) => (c.ariaElements ?? []).flatMap((e) => {
|
|
6528
|
+
const literal = e.role?.literal;
|
|
6529
|
+
if (literal === void 0) return [];
|
|
6530
|
+
const tokens = splitTokens(literal);
|
|
6531
|
+
const badTokens = tokens.filter((t) => !isKnownRole(t) || isAbstractRole(t));
|
|
6532
|
+
if (badTokens.length === 0) return [];
|
|
6533
|
+
return [
|
|
6534
|
+
{
|
|
6535
|
+
line: e.line,
|
|
6536
|
+
message: `role="${literal}" on <${e.tag}> is ${isAbstractRole(badTokens[0]) ? "an abstract role" : "not a WAI-ARIA role"}`
|
|
6537
|
+
}
|
|
6538
|
+
];
|
|
6539
|
+
})
|
|
6540
|
+
});
|
|
6541
|
+
|
|
6542
|
+
// src/rules/a11y/unknown-aria-attribute.ts
|
|
6543
|
+
var a11yUnknownAriaAttribute = componentRule({
|
|
6544
|
+
id: "a11y/unknown-aria-attribute",
|
|
6545
|
+
title: "Unknown ARIA attribute",
|
|
6546
|
+
category: "a11y",
|
|
6547
|
+
label: "Known ARIA attributes",
|
|
6548
|
+
rationale: "An `aria-*` name that does not exist in WAI-ARIA is not recognized by assistive technology, so the attribute is silently ignored instead of doing what the author intended.",
|
|
6549
|
+
recommendation: "Use a spec-defined `aria-*` attribute; unknown names are ignored by assistive technology.",
|
|
6550
|
+
applies: (c) => (c.ariaElements ?? []).some((e) => e.aria.length > 0),
|
|
6551
|
+
bad: (c) => (c.ariaElements ?? []).flatMap(
|
|
6552
|
+
(e) => e.aria.filter((a) => !isKnownAriaAttribute(a.name)).map((a) => ({ line: a.line, message: `\`${a.name}\` is not a WAI-ARIA attribute` }))
|
|
6553
|
+
)
|
|
6554
|
+
});
|
|
6555
|
+
|
|
6556
|
+
// src/rules/a11y/required-aria-props.ts
|
|
6557
|
+
var HOST_SUPPLIED = {
|
|
6558
|
+
"aria-checked": (e) => e.tag === "input" && (e.inputType === "checkbox" || e.inputType === "radio"),
|
|
6559
|
+
"aria-selected": (e) => e.tag === "option",
|
|
6560
|
+
"aria-level": (e) => /^h[1-6]$/.test(e.tag),
|
|
6561
|
+
"aria-valuenow": (e) => e.tag === "input" && e.inputType === "range" || e.tag === "progress" || e.tag === "meter"
|
|
6562
|
+
};
|
|
6563
|
+
var a11yRequiredAriaProps = componentRule({
|
|
6564
|
+
id: "a11y/required-aria-props",
|
|
6565
|
+
title: "Missing required ARIA props",
|
|
6566
|
+
category: "a11y",
|
|
6567
|
+
label: "Required ARIA props",
|
|
6568
|
+
rationale: 'Some WAI-ARIA roles are unusable to assistive technology without their required state/property attributes \u2014 a role="checkbox" with no way to know checked/unchecked announces a control with no discoverable state.',
|
|
6569
|
+
recommendation: "Add the role\u2019s required `aria-*` attribute(s), or rely on native host semantics that already supply them.",
|
|
6570
|
+
applies: (c) => (c.ariaElements ?? []).some((e) => e.role?.literal !== void 0 && !e.role.literal.includes(" ")),
|
|
6571
|
+
bad: (c) => (c.ariaElements ?? []).flatMap((e) => {
|
|
6572
|
+
if (e.hasSpread) return [];
|
|
6573
|
+
const literal = e.role?.literal;
|
|
6574
|
+
if (literal === void 0 || literal.includes(" ")) return [];
|
|
6575
|
+
const required = requiredAriaProps(literal);
|
|
6576
|
+
if (required.length === 0) return [];
|
|
6577
|
+
const present4 = new Set(e.aria.map((a) => a.name));
|
|
6578
|
+
const missing = required.filter((p) => !present4.has(p) && !HOST_SUPPLIED[p]?.(e));
|
|
6579
|
+
if (missing.length === 0) return [];
|
|
6580
|
+
return [{ line: e.line, message: `role="${literal}" on <${e.tag}> is missing required ${missing.join(", ")}` }];
|
|
6581
|
+
})
|
|
6582
|
+
});
|
|
6583
|
+
|
|
6584
|
+
// src/rules/a11y/invalid-aria-value.ts
|
|
6585
|
+
function isValid(type, values, literal) {
|
|
6586
|
+
switch (type) {
|
|
6587
|
+
case "boolean":
|
|
6588
|
+
return literal === "true" || literal === "false";
|
|
6589
|
+
case "tristate":
|
|
6590
|
+
return literal === "true" || literal === "false" || literal === "mixed";
|
|
6591
|
+
case "token":
|
|
6592
|
+
return (values ?? []).includes(literal);
|
|
6593
|
+
case "tokenlist":
|
|
6594
|
+
return splitTokens(literal).every((t) => (values ?? []).includes(t));
|
|
6595
|
+
case "integer":
|
|
6596
|
+
return /^-?\d+$/.test(literal);
|
|
6597
|
+
case "number":
|
|
6598
|
+
return literal.trim() !== "" && Number.isFinite(Number(literal));
|
|
6599
|
+
default:
|
|
6600
|
+
return true;
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
var a11yInvalidAriaValue = componentRule({
|
|
6604
|
+
id: "a11y/invalid-aria-value",
|
|
6605
|
+
title: "Invalid ARIA attribute value",
|
|
6606
|
+
category: "a11y",
|
|
6607
|
+
label: "ARIA attribute values",
|
|
6608
|
+
rationale: "An `aria-*` attribute whose value does not match its spec-defined type (e.g. a boolean given a non-`true`/`false` literal) is misread or ignored by assistive technology.",
|
|
6609
|
+
recommendation: "Use a value matching the attribute\u2019s WAI-ARIA type \u2014 see the spec for allowed values.",
|
|
6610
|
+
applies: (c) => (c.ariaElements ?? []).some((e) => e.aria.some((a) => a.literal !== void 0)),
|
|
6611
|
+
bad: (c) => (c.ariaElements ?? []).flatMap(
|
|
6612
|
+
(e) => e.aria.flatMap((a) => {
|
|
6613
|
+
if (a.literal === void 0) return [];
|
|
6614
|
+
const kind = ariaValueKind(a.name);
|
|
6615
|
+
if (kind === void 0) return [];
|
|
6616
|
+
if (isValid(kind.type, kind.values, a.literal)) return [];
|
|
6617
|
+
return [{ line: a.line, message: `\`${a.name}="${a.literal}"\` is not a valid ${kind.type} value` }];
|
|
6618
|
+
})
|
|
6619
|
+
)
|
|
6620
|
+
});
|
|
6621
|
+
|
|
6622
|
+
// src/rules/a11y/interactive-nesting.ts
|
|
6623
|
+
var a11yInteractiveNesting = componentRule({
|
|
6624
|
+
id: "a11y/interactive-nesting",
|
|
6625
|
+
title: "Interactive element nested in an interactive element",
|
|
6626
|
+
category: "a11y",
|
|
6627
|
+
label: "Interactive nesting",
|
|
6628
|
+
rationale: "A control nested inside another interactive element is unreachable or misannounced by keyboard and assistive technology, and violates the HTML content model.",
|
|
6629
|
+
recommendation: "Restructure the markup so each interactive control is a sibling, not a descendant, of another.",
|
|
6630
|
+
applies: (c) => (c.interactiveNestings ?? []).length > 0,
|
|
6631
|
+
bad: (c) => (c.interactiveNestings ?? []).map((f) => ({
|
|
6632
|
+
line: f.line,
|
|
6633
|
+
message: `<${f.descendantTag}> is nested inside interactive <${f.containerTag}>`
|
|
6634
|
+
}))
|
|
6635
|
+
});
|
|
6636
|
+
|
|
6637
|
+
// src/rules/a11y/accessible-name.ts
|
|
6638
|
+
var a11yAccessibleName = componentRule({
|
|
6639
|
+
id: "a11y/accessible-name",
|
|
6640
|
+
title: "Interactive element has no accessible name",
|
|
6641
|
+
category: "a11y",
|
|
6642
|
+
label: "Accessible names",
|
|
6643
|
+
rationale: 'A button, link, or image button with no accessible name is announced by assistive technology as its bare role ("button", "link") with nothing to distinguish it from any other control on the page.',
|
|
6644
|
+
recommendation: "Give the element visible text, an aria-label/aria-labelledby/title, or an alt on its icon image.",
|
|
6645
|
+
applies: (c) => (c.unnamedInteractive ?? []).length > 0,
|
|
6646
|
+
bad: (c) => (c.unnamedInteractive ?? []).map((f) => ({ line: f.line, message: `<${f.tag}> has no accessible name` }))
|
|
6647
|
+
});
|
|
6648
|
+
|
|
6649
|
+
// src/rules/a11y/label-has-control.ts
|
|
6650
|
+
var a11yLabelHasControl = componentRule({
|
|
6651
|
+
id: "a11y/label-has-control",
|
|
6652
|
+
title: "<label> has no associated control",
|
|
6653
|
+
category: "a11y",
|
|
6654
|
+
label: "Label associations",
|
|
6655
|
+
rationale: "A `<label>` with no associated control is announced by assistive technology as plain text \u2014 clicking or tapping it does not focus the field, and a screen reader gives no relationship between the label and its control.",
|
|
6656
|
+
recommendation: "Add a `for` attribute pointing at the control's `id`, or wrap the control inside the `<label>`.",
|
|
6657
|
+
applies: (c) => (c.unassociatedLabels ?? []).length > 0,
|
|
6658
|
+
bad: (c) => (c.unassociatedLabels ?? []).map((f) => ({ line: f.line, message: "<label> has no associated control" }))
|
|
6659
|
+
});
|
|
6660
|
+
|
|
6661
|
+
// src/rules/a11y/use-list.ts
|
|
6662
|
+
var a11yUseList = componentRule({
|
|
6663
|
+
id: "a11y/use-list",
|
|
6664
|
+
title: "Bullet text should be a list",
|
|
6665
|
+
category: "a11y",
|
|
6666
|
+
severity: "info",
|
|
6667
|
+
label: "List structure",
|
|
6668
|
+
rationale: "A screen reader announces a real `<ul>`/`<ol>` as a list \u2014 item count, position, and boundaries. A bullet character typed into plain text carries none of that, so the visual structure is lost on assistive technology.",
|
|
6669
|
+
recommendation: "Use a `<ul>`/`<ol>` with `<li>` items instead of a bullet character in plain text.",
|
|
6670
|
+
applies: (c) => (c.bulletTexts ?? []).length > 0,
|
|
6671
|
+
bad: (c) => (c.bulletTexts ?? []).map((b) => ({
|
|
6672
|
+
line: b.line,
|
|
6673
|
+
message: `Text starts with a bullet character ('${b.char}') \u2014 use a list element`
|
|
6674
|
+
}))
|
|
6675
|
+
});
|
|
6676
|
+
|
|
6677
|
+
// src/rules/a11y/placeholder-label-option.ts
|
|
6678
|
+
var a11yPlaceholderLabelOption = componentRule({
|
|
6679
|
+
id: "a11y/placeholder-label-option",
|
|
6680
|
+
title: "Missing placeholder label option",
|
|
6681
|
+
category: "a11y",
|
|
6682
|
+
label: "Select placeholder",
|
|
6683
|
+
rationale: "A required, single-selection `<select>` initially shows its first option as the chosen value \u2014 if that option is not an empty placeholder, users can submit the form without ever having made a real choice, and assistive technology announces a value as already selected.",
|
|
6684
|
+
recommendation: 'Make the first `<option>` a placeholder: an empty `value=""`, or no `value` attribute and no text.',
|
|
6685
|
+
applies: (c) => (c.selectsMissingPlaceholder ?? []).length > 0,
|
|
6686
|
+
bad: (c) => (c.selectsMissingPlaceholder ?? []).map((f) => ({
|
|
6687
|
+
line: f.line,
|
|
6688
|
+
message: "<select required> is missing a placeholder label option"
|
|
6689
|
+
}))
|
|
6690
|
+
});
|
|
6691
|
+
|
|
6692
|
+
// src/rules/a11y/require-datetime.ts
|
|
6693
|
+
var a11yRequireDatetime = componentRule({
|
|
6694
|
+
id: "a11y/require-datetime",
|
|
6695
|
+
title: "Missing datetime attribute",
|
|
6696
|
+
category: "a11y",
|
|
6697
|
+
label: "Time elements",
|
|
6698
|
+
rationale: 'A `<time>` element with no `datetime` attribute exposes its text content as the machine-readable value \u2014 text like "last Tuesday" cannot be parsed by assistive technology or user agents into an actual date, so its meaning is lost to anything that isn\'t a sighted reader.',
|
|
6699
|
+
recommendation: 'Add a `datetime` attribute with a machine-readable value, e.g. `<time datetime="2026-08-14">Aug 14</time>`.',
|
|
6700
|
+
applies: (c) => (c.timesMissingDatetime ?? []).length > 0,
|
|
6701
|
+
bad: (c) => (c.timesMissingDatetime ?? []).map((f) => ({
|
|
6702
|
+
line: f.line,
|
|
6703
|
+
message: `<time> content "${f.text}" is not machine-readable and has no datetime attribute`
|
|
6704
|
+
}))
|
|
6705
|
+
});
|
|
6706
|
+
|
|
6707
|
+
// src/rules/a11y/doctype.ts
|
|
6708
|
+
var present3 = { presence: "own", value: "static" };
|
|
6709
|
+
var absent3 = { presence: "none", value: "absent" };
|
|
6710
|
+
var FIX8 = {
|
|
6711
|
+
description: "Add <!doctype html> as the first line of src/app.html.",
|
|
6712
|
+
snippet: "<!doctype html>",
|
|
6713
|
+
lang: "html"
|
|
6714
|
+
};
|
|
6715
|
+
var a11yDoctype = {
|
|
6716
|
+
id: "a11y/doctype",
|
|
6717
|
+
title: "Doctype",
|
|
6718
|
+
category: "a11y",
|
|
6719
|
+
severity: "warning",
|
|
6720
|
+
scope: "project",
|
|
6721
|
+
rationale: "Without a doctype browsers render in quirks mode, breaking CSS and accessibility tree behavior.",
|
|
6722
|
+
fix: FIX8,
|
|
6723
|
+
async check(ctx) {
|
|
6724
|
+
const { appHtmlDoctype } = ctx.project;
|
|
6725
|
+
if (appHtmlDoctype === void 0) return [];
|
|
6726
|
+
return [
|
|
6727
|
+
{
|
|
6728
|
+
id: "a11y/doctype",
|
|
6729
|
+
category: "a11y",
|
|
6730
|
+
severity: "warning",
|
|
6731
|
+
detection: appHtmlDoctype ? present3 : absent3,
|
|
6732
|
+
location: "src/app.html",
|
|
6733
|
+
message: appHtmlDoctype ? "<!doctype html>" : "src/app.html is missing <!doctype html>",
|
|
6734
|
+
recommendation: "Add <!doctype html> as the first line of src/app.html.",
|
|
6735
|
+
docsUrl: docsUrlFor("a11y/doctype"),
|
|
6736
|
+
fix: { ...FIX8 }
|
|
6737
|
+
}
|
|
6738
|
+
];
|
|
6739
|
+
}
|
|
6740
|
+
};
|
|
6741
|
+
|
|
6742
|
+
// src/rules/a11y/route-rule.ts
|
|
6743
|
+
function resultFactory(id, recommendation12) {
|
|
6744
|
+
const docsUrl12 = docsUrlFor(id);
|
|
6745
|
+
return (route, detection, occ, message) => ({
|
|
6746
|
+
id,
|
|
6747
|
+
category: "a11y",
|
|
6748
|
+
severity: "warning",
|
|
6749
|
+
detection,
|
|
6750
|
+
route,
|
|
6751
|
+
location: occ.file,
|
|
6752
|
+
...occ.line > 0 ? { line: occ.line } : {},
|
|
6753
|
+
message,
|
|
6754
|
+
recommendation: recommendation12,
|
|
6755
|
+
docsUrl: docsUrl12
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
function surplusRule(spec) {
|
|
6759
|
+
const result3 = resultFactory(spec.id, spec.recommendation);
|
|
6760
|
+
return {
|
|
6761
|
+
id: spec.id,
|
|
6762
|
+
title: spec.title,
|
|
6763
|
+
category: "a11y",
|
|
6764
|
+
severity: "warning",
|
|
6765
|
+
scope: "route",
|
|
6766
|
+
rationale: spec.rationale,
|
|
6767
|
+
async check(ctx) {
|
|
6768
|
+
const out = [];
|
|
6769
|
+
for (const route of ctx.a11y ?? []) {
|
|
6770
|
+
let first;
|
|
6771
|
+
let surplus = false;
|
|
6772
|
+
for (const [key, reps] of spec.map(route)) {
|
|
6773
|
+
first ??= reps[0];
|
|
6774
|
+
for (let i = 1; i < reps.length; i++) {
|
|
6775
|
+
surplus = true;
|
|
6776
|
+
out.push(result3(route.route, PENALIZED, reps[i], spec.message(key, i, reps.length)));
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
if (first && !surplus) out.push(result3(route.route, PASS, { file: first.file, line: 0 }, spec.passMessage));
|
|
6780
|
+
}
|
|
6781
|
+
return out;
|
|
6782
|
+
}
|
|
6783
|
+
};
|
|
6784
|
+
}
|
|
6785
|
+
|
|
6786
|
+
// src/rules/a11y/duplicate-landmark.ts
|
|
6787
|
+
var KINDS = ["main", "banner", "contentinfo"];
|
|
6788
|
+
var a11yDuplicateLandmark = surplusRule({
|
|
6789
|
+
id: "a11y/duplicate-landmark",
|
|
6790
|
+
title: "Duplicate landmark",
|
|
6791
|
+
rationale: "Assistive tech users jump between landmarks to skip repeated content; more than one main, banner, or contentinfo per page leaves them guessing which one is the real one.",
|
|
6792
|
+
recommendation: "A route should have at most one main, banner, and contentinfo landmark.",
|
|
6793
|
+
map: (route) => KINDS.flatMap((kind) => {
|
|
6794
|
+
const reps = route.landmarks[kind];
|
|
6795
|
+
return reps?.length ? [[kind, reps]] : [];
|
|
6796
|
+
}),
|
|
6797
|
+
message: (kind, i, n) => `Duplicate ${kind} landmark (${i + 1} of ${n})`,
|
|
6798
|
+
passMessage: "No duplicate landmarks"
|
|
6799
|
+
});
|
|
6800
|
+
|
|
6801
|
+
// src/rules/a11y/top-level-landmark.ts
|
|
6802
|
+
var recommendation10 = "A banner, main, complementary, or contentinfo landmark should not be nested inside another landmark.";
|
|
6803
|
+
var result = resultFactory("a11y/top-level-landmark", recommendation10);
|
|
6804
|
+
var KINDS2 = ["main", "banner", "complementary", "contentinfo"];
|
|
6805
|
+
var a11yTopLevelLandmark = {
|
|
6806
|
+
id: "a11y/top-level-landmark",
|
|
6807
|
+
title: "Top-level landmark",
|
|
6808
|
+
category: "a11y",
|
|
6809
|
+
severity: "warning",
|
|
6810
|
+
scope: "route",
|
|
6811
|
+
rationale: "Assistive tech landmark navigation expects banner/main/complementary/contentinfo at the top level; nesting one inside another hides it from that navigation.",
|
|
6812
|
+
async check(ctx) {
|
|
6813
|
+
const out = [];
|
|
6814
|
+
for (const route of ctx.a11y ?? []) {
|
|
6815
|
+
for (const nested of route.nestedLandmarks) {
|
|
6816
|
+
out.push(result(route.route, PENALIZED, nested, `${nested.kind} landmark is nested inside ${nested.within}`));
|
|
6817
|
+
}
|
|
6818
|
+
if (route.nestedLandmarks.length === 0) {
|
|
6819
|
+
const first = KINDS2.map((kind) => route.landmarks[kind]?.[0]).find((rep) => rep !== void 0);
|
|
6820
|
+
if (first) out.push(result(route.route, PASS, { file: first.file, line: 0 }, "No nested landmarks"));
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
return out;
|
|
6824
|
+
}
|
|
6825
|
+
};
|
|
6826
|
+
|
|
6827
|
+
// src/rules/a11y/id-duplication.ts
|
|
6828
|
+
var a11yIdDuplication = surplusRule({
|
|
6829
|
+
id: "a11y/id-duplication",
|
|
6830
|
+
title: "Id duplication",
|
|
6831
|
+
rationale: "A duplicate id breaks label/aria-labelledby associations and in-page fragment navigation: assistive tech resolves the first match, which may not be the one the author intended.",
|
|
6832
|
+
recommendation: "Every id in a route should be unique.",
|
|
6833
|
+
// Entries ordered by each id's first representative (file, then line): content-derived and
|
|
6834
|
+
// stable — a Record's own-key enumeration would pull integer-like ids ("1") to the front.
|
|
6835
|
+
map: (route) => Object.entries(route.ids).sort(([, a], [, b]) => a[0].file.localeCompare(b[0].file) || a[0].line - b[0].line),
|
|
6836
|
+
message: (id) => `Duplicate id "${id}"`,
|
|
6837
|
+
passMessage: "No duplicate ids"
|
|
6838
|
+
});
|
|
6839
|
+
|
|
6840
|
+
// src/rules/a11y/no-missing-id-ref.ts
|
|
6841
|
+
var recommendation11 = "An id reference should point to an id that exists somewhere in the composed route.";
|
|
6842
|
+
var result2 = resultFactory("a11y/no-missing-id-ref", recommendation11);
|
|
6843
|
+
var a11yNoMissingIdRef = {
|
|
6844
|
+
id: "a11y/no-missing-id-ref",
|
|
6845
|
+
title: "No missing id ref",
|
|
6846
|
+
category: "a11y",
|
|
6847
|
+
severity: "warning",
|
|
6848
|
+
scope: "route",
|
|
6849
|
+
rationale: 'A `for`/`aria-labelledby`/`aria-describedby`/`aria-controls`/`aria-activedescendant`/`href="#\u2026"` pointing at an id that does not exist leaves assistive tech with a broken association or the browser with a dead in-page link.',
|
|
6850
|
+
async check(ctx) {
|
|
6851
|
+
const out = [];
|
|
6852
|
+
for (const route of ctx.a11y ?? []) {
|
|
6853
|
+
if (!route.fullyResolved || route.idRefs.length === 0) continue;
|
|
6854
|
+
const candidates = new Set(route.idCandidates);
|
|
6855
|
+
let hasMissing = false;
|
|
6856
|
+
for (const ref of route.idRefs) {
|
|
6857
|
+
if (candidates.has(ref.id)) continue;
|
|
6858
|
+
hasMissing = true;
|
|
6859
|
+
out.push(
|
|
6860
|
+
result2(
|
|
6861
|
+
route.route,
|
|
6862
|
+
PENALIZED,
|
|
6863
|
+
ref,
|
|
6864
|
+
`${ref.attr}="${ref.attr === "href" ? "#" : ""}${ref.id}" references a missing id`
|
|
6865
|
+
)
|
|
6866
|
+
);
|
|
6867
|
+
}
|
|
6868
|
+
if (!hasMissing) {
|
|
6869
|
+
const first = route.idRefs[0];
|
|
6870
|
+
out.push(result2(route.route, PASS, { file: first.file, line: 0 }, "No missing id references"));
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
return out;
|
|
6874
|
+
}
|
|
6875
|
+
};
|
|
6876
|
+
|
|
6055
6877
|
// src/rules/index.ts
|
|
6056
6878
|
var allRules = [
|
|
6057
6879
|
seoTitlePresence,
|
|
@@ -6126,7 +6948,22 @@ var allRules = [
|
|
|
6126
6948
|
performanceMinifyDisabled,
|
|
6127
6949
|
performanceLoadWaterfall,
|
|
6128
6950
|
performanceSequentialAwaits,
|
|
6129
|
-
performanceStateRaw
|
|
6951
|
+
performanceStateRaw,
|
|
6952
|
+
a11yInvalidRole,
|
|
6953
|
+
a11yUnknownAriaAttribute,
|
|
6954
|
+
a11yRequiredAriaProps,
|
|
6955
|
+
a11yInvalidAriaValue,
|
|
6956
|
+
a11yInteractiveNesting,
|
|
6957
|
+
a11yAccessibleName,
|
|
6958
|
+
a11yLabelHasControl,
|
|
6959
|
+
a11yUseList,
|
|
6960
|
+
a11yPlaceholderLabelOption,
|
|
6961
|
+
a11yRequireDatetime,
|
|
6962
|
+
a11yDoctype,
|
|
6963
|
+
a11yDuplicateLandmark,
|
|
6964
|
+
a11yTopLevelLandmark,
|
|
6965
|
+
a11yIdDuplication,
|
|
6966
|
+
a11yNoMissingIdRef
|
|
6130
6967
|
];
|
|
6131
6968
|
function optionInfos(spec) {
|
|
6132
6969
|
return Object.entries(spec).map(([name, s]) => ({
|
|
@@ -6153,21 +6990,21 @@ function explainRule(id) {
|
|
|
6153
6990
|
}
|
|
6154
6991
|
|
|
6155
6992
|
// src/summary.ts
|
|
6156
|
-
function classify(
|
|
6157
|
-
if (isPenalized(
|
|
6158
|
-
if (
|
|
6993
|
+
function classify(result3, config) {
|
|
6994
|
+
if (isPenalized(result3.detection, config.treatDynamicAs)) return "fail";
|
|
6995
|
+
if (result3.detection.value === "dynamic") return "dynamic";
|
|
6159
6996
|
return "pass";
|
|
6160
6997
|
}
|
|
6161
|
-
function effectiveSeverity(
|
|
6162
|
-
if (
|
|
6163
|
-
return
|
|
6998
|
+
function effectiveSeverity(result3, config) {
|
|
6999
|
+
if (result3.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
|
|
7000
|
+
return result3.severity;
|
|
6164
7001
|
}
|
|
6165
7002
|
function summarize(results, config) {
|
|
6166
7003
|
const summary = { critical: 0, warning: 0, info: 0, passed: 0, dynamic: 0 };
|
|
6167
|
-
for (const
|
|
6168
|
-
const cls = classify(
|
|
7004
|
+
for (const result3 of results) {
|
|
7005
|
+
const cls = classify(result3, config);
|
|
6169
7006
|
if (cls === "fail") {
|
|
6170
|
-
summary[effectiveSeverity(
|
|
7007
|
+
summary[effectiveSeverity(result3, config)] += 1;
|
|
6171
7008
|
} else {
|
|
6172
7009
|
summary.passed += 1;
|
|
6173
7010
|
if (cls === "dynamic") summary.dynamic += 1;
|
|
@@ -6341,7 +7178,16 @@ var SEVERITY_TITLE = {
|
|
|
6341
7178
|
warning: "Warnings",
|
|
6342
7179
|
info: "Info"
|
|
6343
7180
|
};
|
|
6344
|
-
var
|
|
7181
|
+
var CATEGORY_LABEL = {
|
|
7182
|
+
seo: "SEO",
|
|
7183
|
+
performance: "Performance",
|
|
7184
|
+
correctness: "Correctness",
|
|
7185
|
+
security: "Security",
|
|
7186
|
+
architecture: "Architecture",
|
|
7187
|
+
a11y: "Accessibility"
|
|
7188
|
+
};
|
|
7189
|
+
var CATEGORY_ORDER = Object.keys(CATEGORY_LABEL);
|
|
7190
|
+
var categoryLabel = (c) => CATEGORY_LABEL[c];
|
|
6345
7191
|
var MAX_RULE_GROUPS_PER_BUCKET = 5;
|
|
6346
7192
|
function groupByRule(results) {
|
|
6347
7193
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -6396,7 +7242,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
6396
7242
|
const p = options.palette ?? noColorPalette;
|
|
6397
7243
|
const summary = summarize(results, config);
|
|
6398
7244
|
const { health, categories: byCat } = computeHealth(results, config);
|
|
6399
|
-
const
|
|
7245
|
+
const present4 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
6400
7246
|
const lines = [];
|
|
6401
7247
|
if (!options.omitHeader) {
|
|
6402
7248
|
lines.push(
|
|
@@ -6405,7 +7251,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
6405
7251
|
`${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
|
|
6406
7252
|
);
|
|
6407
7253
|
}
|
|
6408
|
-
for (const c of
|
|
7254
|
+
for (const c of present4) {
|
|
6409
7255
|
lines.push(scoreLine(p, categoryLabel(c), byCat[c]));
|
|
6410
7256
|
}
|
|
6411
7257
|
lines.push("");
|
|
@@ -6465,17 +7311,17 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
6465
7311
|
}
|
|
6466
7312
|
|
|
6467
7313
|
// src/reporter/json.ts
|
|
6468
|
-
function issueOf(
|
|
7314
|
+
function issueOf(result3) {
|
|
6469
7315
|
return {
|
|
6470
|
-
id:
|
|
6471
|
-
category:
|
|
6472
|
-
title:
|
|
6473
|
-
detection:
|
|
6474
|
-
location:
|
|
6475
|
-
...
|
|
6476
|
-
recommendation:
|
|
6477
|
-
...
|
|
6478
|
-
...
|
|
7316
|
+
id: result3.id,
|
|
7317
|
+
category: result3.category ?? "seo",
|
|
7318
|
+
title: result3.message,
|
|
7319
|
+
detection: result3.detection,
|
|
7320
|
+
location: result3.location,
|
|
7321
|
+
...result3.line !== void 0 ? { line: result3.line } : {},
|
|
7322
|
+
recommendation: result3.recommendation,
|
|
7323
|
+
...result3.docsUrl ? { docsUrl: result3.docsUrl } : {},
|
|
7324
|
+
...result3.fix ? { fix: result3.fix } : {}
|
|
6479
7325
|
};
|
|
6480
7326
|
}
|
|
6481
7327
|
function ruleEvidence(results, config, ruleIds) {
|
|
@@ -6543,8 +7389,8 @@ function severityToSarifLevel(sev) {
|
|
|
6543
7389
|
function severityToGithubLevel(sev) {
|
|
6544
7390
|
return sev === "critical" ? "error" : sev === "warning" ? "warning" : "notice";
|
|
6545
7391
|
}
|
|
6546
|
-
function messageText(
|
|
6547
|
-
return
|
|
7392
|
+
function messageText(result3) {
|
|
7393
|
+
return result3.recommendation ? `${result3.message} ${result3.recommendation}` : result3.message;
|
|
6548
7394
|
}
|
|
6549
7395
|
var RULE_META = new Map(
|
|
6550
7396
|
allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor(r.id) }])
|
|
@@ -6614,7 +7460,7 @@ function formatSarifReport(results, config, meta) {
|
|
|
6614
7460
|
defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
|
|
6615
7461
|
});
|
|
6616
7462
|
}
|
|
6617
|
-
const
|
|
7463
|
+
const result3 = {
|
|
6618
7464
|
ruleId: r.id,
|
|
6619
7465
|
ruleIndex: ruleIndex.get(r.id),
|
|
6620
7466
|
level: severityToSarifLevel(effectiveSeverity(r, config)),
|
|
@@ -6624,7 +7470,7 @@ function formatSarifReport(results, config, meta) {
|
|
|
6624
7470
|
}
|
|
6625
7471
|
};
|
|
6626
7472
|
if (r.location) {
|
|
6627
|
-
|
|
7473
|
+
result3.locations = [
|
|
6628
7474
|
{
|
|
6629
7475
|
physicalLocation: {
|
|
6630
7476
|
artifactLocation: { uri: r.location },
|
|
@@ -6633,7 +7479,7 @@ function formatSarifReport(results, config, meta) {
|
|
|
6633
7479
|
}
|
|
6634
7480
|
];
|
|
6635
7481
|
}
|
|
6636
|
-
return
|
|
7482
|
+
return result3;
|
|
6637
7483
|
});
|
|
6638
7484
|
const log = {
|
|
6639
7485
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
@@ -6899,6 +7745,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
|
|
|
6899
7745
|
var APP_SCRIPT = `
|
|
6900
7746
|
(function(){
|
|
6901
7747
|
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
7748
|
+
var CATEGORY_NAMES = ${JSON.stringify(CATEGORY_LABEL)};
|
|
6902
7749
|
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
6903
7750
|
|
|
6904
7751
|
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
|
|
@@ -7245,7 +8092,7 @@ var APP_SCRIPT = `
|
|
|
7245
8092
|
}, []);
|
|
7246
8093
|
};
|
|
7247
8094
|
var catChips = Object.keys(categories).map(function (cat) {
|
|
7248
|
-
var name = cat
|
|
8095
|
+
var name = CATEGORY_NAMES[cat] || cat;
|
|
7249
8096
|
return chip(cat, name);
|
|
7250
8097
|
});
|
|
7251
8098
|
return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
|
|
@@ -7317,7 +8164,7 @@ var APP_SCRIPT = `
|
|
|
7317
8164
|
var c = s.report.categories[cat];
|
|
7318
8165
|
var band = scoreBand(c.score);
|
|
7319
8166
|
var weight = s.report.weights[cat];
|
|
7320
|
-
var name = cat
|
|
8167
|
+
var name = CATEGORY_NAMES[cat] || cat;
|
|
7321
8168
|
// keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) \u2014
|
|
7322
8169
|
// render nothing rather than "undefined of undefined". 0 affected of N keys is still
|
|
7323
8170
|
// rendered: on a real project that's the signal a thin score can't give, that the
|
|
@@ -7458,10 +8305,27 @@ export {
|
|
|
7458
8305
|
BAND_COLOR,
|
|
7459
8306
|
CATEGORIES,
|
|
7460
8307
|
CHILD_NODE_KEYS,
|
|
8308
|
+
IDREF_ATTRS,
|
|
8309
|
+
LANDMARK_ROLES,
|
|
7461
8310
|
ROBOTS_SOURCE_PATHS,
|
|
7462
8311
|
SITEMAP_SOURCE_PATHS,
|
|
7463
8312
|
SVELTE_CONFIG_FILES,
|
|
7464
8313
|
VITE_CONFIG_FILES,
|
|
8314
|
+
a11yAccessibleName,
|
|
8315
|
+
a11yDoctype,
|
|
8316
|
+
a11yDuplicateLandmark,
|
|
8317
|
+
a11yIdDuplication,
|
|
8318
|
+
a11yInteractiveNesting,
|
|
8319
|
+
a11yInvalidAriaValue,
|
|
8320
|
+
a11yInvalidRole,
|
|
8321
|
+
a11yLabelHasControl,
|
|
8322
|
+
a11yNoMissingIdRef,
|
|
8323
|
+
a11yPlaceholderLabelOption,
|
|
8324
|
+
a11yRequireDatetime,
|
|
8325
|
+
a11yRequiredAriaProps,
|
|
8326
|
+
a11yTopLevelLandmark,
|
|
8327
|
+
a11yUnknownAriaAttribute,
|
|
8328
|
+
a11yUseList,
|
|
7465
8329
|
allRules,
|
|
7466
8330
|
applyOverrides,
|
|
7467
8331
|
applyRuleSeverities,
|
|
@@ -7501,6 +8365,7 @@ export {
|
|
|
7501
8365
|
correctnessServerBrowserGlobal,
|
|
7502
8366
|
correctnessStalePropDerivation,
|
|
7503
8367
|
correctnessUnmutatedState,
|
|
8368
|
+
decodeFragmentId,
|
|
7504
8369
|
defaultConfig,
|
|
7505
8370
|
defaultProject,
|
|
7506
8371
|
defineConfig,
|
|
@@ -7515,6 +8380,7 @@ export {
|
|
|
7515
8380
|
findKitPathsBaseInSvelteConfig,
|
|
7516
8381
|
findKitPathsBaseInViteConfig,
|
|
7517
8382
|
findMinifyDisabled,
|
|
8383
|
+
foldOccurrences,
|
|
7518
8384
|
formatAgentReport,
|
|
7519
8385
|
formatConsoleReport,
|
|
7520
8386
|
formatFailedRuleWarning,
|
|
@@ -7529,6 +8395,7 @@ export {
|
|
|
7529
8395
|
intOption,
|
|
7530
8396
|
isMentionedAnywhere,
|
|
7531
8397
|
isPenalized,
|
|
8398
|
+
isTopFragment,
|
|
7532
8399
|
lineOf,
|
|
7533
8400
|
linkRule,
|
|
7534
8401
|
listOption,
|
|
@@ -7602,6 +8469,7 @@ export {
|
|
|
7602
8469
|
settingOptions,
|
|
7603
8470
|
settingSeverity,
|
|
7604
8471
|
shouldSkipRangeCheck,
|
|
8472
|
+
splitTokens,
|
|
7605
8473
|
summarize,
|
|
7606
8474
|
terminalSafe,
|
|
7607
8475
|
textFromNodes,
|