a11ylens 0.1.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +162 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +9 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +104 -0
- package/dist/engine/runA11yLens.d.ts +9 -0
- package/dist/engine/runA11yLens.js +158 -0
- package/dist/engine/styleResolver.d.ts +2 -0
- package/dist/engine/styleResolver.js +63 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/parser/ast.d.ts +25 -0
- package/dist/parser/ast.js +169 -0
- package/dist/parser/css.d.ts +7 -0
- package/dist/parser/css.js +48 -0
- package/dist/reporters/consoleReporter.d.ts +2 -0
- package/dist/reporters/consoleReporter.js +67 -0
- package/dist/reporters/jsonReporter.d.ts +2 -0
- package/dist/reporters/jsonReporter.js +7 -0
- package/dist/reporters/sarifReporter.d.ts +2 -0
- package/dist/reporters/sarifReporter.js +95 -0
- package/dist/rules/clickableNonsemantic.d.ts +2 -0
- package/dist/rules/clickableNonsemantic.js +43 -0
- package/dist/rules/contrastRatio.d.ts +2 -0
- package/dist/rules/contrastRatio.js +149 -0
- package/dist/rules/engine.d.ts +4 -0
- package/dist/rules/engine.js +22 -0
- package/dist/rules/iconOnlyControl.d.ts +2 -0
- package/dist/rules/iconOnlyControl.js +50 -0
- package/dist/rules/imgAlt.d.ts +2 -0
- package/dist/rules/imgAlt.js +48 -0
- package/dist/rules/registry.d.ts +3 -0
- package/dist/rules/registry.js +19 -0
- package/dist/rules/routerLinkText.d.ts +2 -0
- package/dist/rules/routerLinkText.js +32 -0
- package/dist/rules/utils.d.ts +6 -0
- package/dist/rules/utils.js +51 -0
- package/dist/scanner/fileAnalyzer.d.ts +1 -0
- package/dist/scanner/fileAnalyzer.js +46 -0
- package/dist/scanner/fileSystemWalker.js +122 -0
- package/dist/scanner/fileWalker.d.ts +8 -0
- package/dist/scanner/fileWalker.js +126 -0
- package/dist/scanner/styleExtractor.d.ts +13 -0
- package/dist/scanner/styleExtractor.js +95 -0
- package/dist/scanner/templateExtractor.d.ts +13 -0
- package/dist/scanner/templateExtractor.js +101 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +2 -0
- package/dist/types/results.d.ts +22 -0
- package/dist/types/results.js +2 -0
- package/dist/types/rules.d.ts +14 -0
- package/dist/types/rules.js +2 -0
- package/dist/types/style.d.ts +9 -0
- package/dist/types/style.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.contrastRatioRule = void 0;
|
|
4
|
+
const css_1 = require("../parser/css");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const parseHexChannel = (value) => {
|
|
7
|
+
const parsed = Number.parseInt(value, 16);
|
|
8
|
+
if (Number.isNaN(parsed))
|
|
9
|
+
return undefined;
|
|
10
|
+
return parsed;
|
|
11
|
+
};
|
|
12
|
+
const parseColor = (value) => {
|
|
13
|
+
const normalized = value.trim().toLowerCase();
|
|
14
|
+
if (normalized.startsWith("#")) {
|
|
15
|
+
const hex = normalized.slice(1);
|
|
16
|
+
if (hex.length === 3) {
|
|
17
|
+
const r = parseHexChannel(hex[0] + hex[0]);
|
|
18
|
+
const g = parseHexChannel(hex[1] + hex[1]);
|
|
19
|
+
const b = parseHexChannel(hex[2] + hex[2]);
|
|
20
|
+
if (r === undefined || g === undefined || b === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return [r, g, b];
|
|
23
|
+
}
|
|
24
|
+
if (hex.length === 6) {
|
|
25
|
+
const r = parseHexChannel(hex.slice(0, 2));
|
|
26
|
+
const g = parseHexChannel(hex.slice(2, 4));
|
|
27
|
+
const b = parseHexChannel(hex.slice(4, 6));
|
|
28
|
+
if (r === undefined || g === undefined || b === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
return [r, g, b];
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const rgbMatch = normalized.match(/^rgba?\(([^)]+)\)$/);
|
|
35
|
+
if (rgbMatch) {
|
|
36
|
+
const parts = rgbMatch[1].split(",").map((item) => item.trim());
|
|
37
|
+
if (parts.length < 3)
|
|
38
|
+
return undefined;
|
|
39
|
+
const r = Number(parts[0]);
|
|
40
|
+
const g = Number(parts[1]);
|
|
41
|
+
const b = Number(parts[2]);
|
|
42
|
+
if ([r, g, b].some((channel) => Number.isNaN(channel)))
|
|
43
|
+
return undefined;
|
|
44
|
+
return [clampChannel(r), clampChannel(g), clampChannel(b)];
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const clampChannel = (value) => {
|
|
49
|
+
if (value < 0)
|
|
50
|
+
return 0;
|
|
51
|
+
if (value > 255)
|
|
52
|
+
return 255;
|
|
53
|
+
return Math.round(value);
|
|
54
|
+
};
|
|
55
|
+
const channelToLinear = (value) => {
|
|
56
|
+
const s = value / 255;
|
|
57
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
58
|
+
};
|
|
59
|
+
const luminance = (rgb) => {
|
|
60
|
+
const [r, g, b] = rgb.map(channelToLinear);
|
|
61
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
62
|
+
};
|
|
63
|
+
const contrastRatio = (fg, bg) => {
|
|
64
|
+
const l1 = luminance(fg);
|
|
65
|
+
const l2 = luminance(bg);
|
|
66
|
+
const lighter = Math.max(l1, l2);
|
|
67
|
+
const darker = Math.min(l1, l2);
|
|
68
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
69
|
+
};
|
|
70
|
+
const isLargeText = (styles) => {
|
|
71
|
+
const fontSize = styles.fontSize;
|
|
72
|
+
const fontWeight = styles.fontWeight;
|
|
73
|
+
let sizePx;
|
|
74
|
+
if (fontSize && fontSize.endsWith("px")) {
|
|
75
|
+
const parsed = Number(fontSize.slice(0, -2));
|
|
76
|
+
if (!Number.isNaN(parsed))
|
|
77
|
+
sizePx = parsed;
|
|
78
|
+
}
|
|
79
|
+
const weight = fontWeight ? Number(fontWeight) : undefined;
|
|
80
|
+
const isBold = weight !== undefined && weight >= 700;
|
|
81
|
+
if (sizePx === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (sizePx >= 18)
|
|
84
|
+
return true;
|
|
85
|
+
if (isBold && sizePx >= 14)
|
|
86
|
+
return true;
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
const getInlineContrast = (node, styles) => {
|
|
90
|
+
const colorValue = styles.color;
|
|
91
|
+
const backgroundValue = styles.backgroundColor;
|
|
92
|
+
if (!colorValue || !backgroundValue)
|
|
93
|
+
return undefined;
|
|
94
|
+
const fg = parseColor(colorValue);
|
|
95
|
+
const bg = parseColor(backgroundValue);
|
|
96
|
+
if (!fg || !bg)
|
|
97
|
+
return undefined;
|
|
98
|
+
const ratio = contrastRatio(fg, bg);
|
|
99
|
+
const threshold = isLargeText(styles) ? 3 : 4.5;
|
|
100
|
+
return { ratio, threshold };
|
|
101
|
+
};
|
|
102
|
+
const getResolvedStyles = (node, styleLookup) => {
|
|
103
|
+
var _a, _b, _c, _d;
|
|
104
|
+
const base = styleLookup ? styleLookup.getStyleFor(node) : {};
|
|
105
|
+
const styleAttr = (0, utils_1.getAttr)(node, "style");
|
|
106
|
+
if (typeof styleAttr !== "string")
|
|
107
|
+
return base;
|
|
108
|
+
const inline = (0, css_1.parseInlineStyle)(styleAttr);
|
|
109
|
+
return {
|
|
110
|
+
color: (_a = inline.color) !== null && _a !== void 0 ? _a : base.color,
|
|
111
|
+
backgroundColor: (_b = inline.backgroundColor) !== null && _b !== void 0 ? _b : base.backgroundColor,
|
|
112
|
+
fontSize: (_c = inline.fontSize) !== null && _c !== void 0 ? _c : base.fontSize,
|
|
113
|
+
fontWeight: (_d = inline.fontWeight) !== null && _d !== void 0 ? _d : base.fontWeight
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const getContrast = (node, styleLookup) => {
|
|
117
|
+
const resolved = getResolvedStyles(node, styleLookup);
|
|
118
|
+
return getInlineContrast(node, resolved);
|
|
119
|
+
};
|
|
120
|
+
exports.contrastRatioRule = {
|
|
121
|
+
id: "color-contrast",
|
|
122
|
+
description: "Text color should meet WCAG AA contrast ratios.",
|
|
123
|
+
severity: "warn",
|
|
124
|
+
check: (ast, context) => {
|
|
125
|
+
const issues = [];
|
|
126
|
+
(0, utils_1.walkWithAncestors)(ast, (node) => {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
if (node.type !== "element")
|
|
129
|
+
return;
|
|
130
|
+
const text = (0, utils_1.getAccessibleText)(node).replace(/\s+/g, " ").trim();
|
|
131
|
+
if (!text)
|
|
132
|
+
return;
|
|
133
|
+
const contrast = getContrast(node, context.styleLookup);
|
|
134
|
+
if (!contrast)
|
|
135
|
+
return;
|
|
136
|
+
if (contrast.ratio < contrast.threshold) {
|
|
137
|
+
issues.push({
|
|
138
|
+
ruleId: "color-contrast",
|
|
139
|
+
message: `Color contrast ${contrast.ratio.toFixed(2)}:1 is below ${contrast.threshold}:1.`,
|
|
140
|
+
filePath: context.filePath,
|
|
141
|
+
line: (_a = node.loc) === null || _a === void 0 ? void 0 : _a.line,
|
|
142
|
+
column: (_b = node.loc) === null || _b === void 0 ? void 0 : _b.column,
|
|
143
|
+
severity: "warn"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return issues;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Issue } from "../types/results";
|
|
2
|
+
import type { Rule } from "../types/rules";
|
|
3
|
+
import type { RootNode } from "../parser/ast";
|
|
4
|
+
export declare const runRules: (rules: Rule[], ast: RootNode, filePath: string, source: string, styleLookup?: import("../types/style").StyleLookup) => Issue[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRules = void 0;
|
|
4
|
+
const runRules = (rules, ast, filePath, source, styleLookup) => {
|
|
5
|
+
const issues = [];
|
|
6
|
+
for (const rule of rules) {
|
|
7
|
+
try {
|
|
8
|
+
const results = rule.check(ast, { filePath, source, styleLookup });
|
|
9
|
+
issues.push(...results.map((issue) => { var _a; return ({ ...issue, severity: (_a = issue.severity) !== null && _a !== void 0 ? _a : rule.severity }); }));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
issues.push({
|
|
13
|
+
ruleId: rule.id,
|
|
14
|
+
message: `Rule ${rule.id} failed to execute.`,
|
|
15
|
+
filePath,
|
|
16
|
+
severity: "warn"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return issues;
|
|
21
|
+
};
|
|
22
|
+
exports.runRules = runRules;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.iconOnlyControlRule = void 0;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const iconTags = new Set(["i", "svg", "mat-icon", "fa-icon"]);
|
|
6
|
+
const hasText = (node) => {
|
|
7
|
+
const text = (0, utils_1.getAccessibleText)(node).replace(/\s+/g, " ").trim();
|
|
8
|
+
return text.length > 0;
|
|
9
|
+
};
|
|
10
|
+
const isOnlyIconChildren = (node) => {
|
|
11
|
+
const elementChildren = node.children.filter((child) => child.type === "element");
|
|
12
|
+
if (elementChildren.length === 0)
|
|
13
|
+
return false;
|
|
14
|
+
for (const child of elementChildren) {
|
|
15
|
+
if (!iconTags.has(child.tagName))
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
};
|
|
20
|
+
exports.iconOnlyControlRule = {
|
|
21
|
+
id: "icon-only-control",
|
|
22
|
+
description: "Controls with only icons need accessible labels.",
|
|
23
|
+
severity: "warn",
|
|
24
|
+
check: (ast, context) => {
|
|
25
|
+
const issues = [];
|
|
26
|
+
(0, utils_1.walkWithAncestors)(ast, (node) => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
if (node.type !== "element")
|
|
29
|
+
return;
|
|
30
|
+
if (node.tagName !== "button" && node.tagName !== "a")
|
|
31
|
+
return;
|
|
32
|
+
const ariaLabel = (0, utils_1.getAttr)(node, "aria-label");
|
|
33
|
+
if (typeof ariaLabel === "string" && ariaLabel.trim().length > 0)
|
|
34
|
+
return;
|
|
35
|
+
if (hasText(node))
|
|
36
|
+
return;
|
|
37
|
+
if (!isOnlyIconChildren(node))
|
|
38
|
+
return;
|
|
39
|
+
issues.push({
|
|
40
|
+
ruleId: "icon-only-control",
|
|
41
|
+
message: "Icon-only controls need aria-label text.",
|
|
42
|
+
filePath: context.filePath,
|
|
43
|
+
line: (_a = node.loc) === null || _a === void 0 ? void 0 : _a.line,
|
|
44
|
+
column: (_b = node.loc) === null || _b === void 0 ? void 0 : _b.column,
|
|
45
|
+
severity: "warn"
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.imgAltRule = void 0;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const hasAccessibleName = (node) => {
|
|
6
|
+
const ariaLabel = (0, utils_1.getAttr)(node, "aria-label");
|
|
7
|
+
const text = (0, utils_1.getAccessibleText)(node).replace(/\s+/g, " ").trim();
|
|
8
|
+
return (typeof ariaLabel === "string" && ariaLabel.trim().length > 0) || text.length > 0;
|
|
9
|
+
};
|
|
10
|
+
exports.imgAltRule = {
|
|
11
|
+
id: "img-alt",
|
|
12
|
+
description: "Images must have alternative text.",
|
|
13
|
+
severity: "error",
|
|
14
|
+
check: (ast, context) => {
|
|
15
|
+
const issues = [];
|
|
16
|
+
(0, utils_1.walkWithAncestors)(ast, (node, ancestors) => {
|
|
17
|
+
var _a, _b, _c, _d;
|
|
18
|
+
if (node.type !== "element" || node.tagName !== "img")
|
|
19
|
+
return;
|
|
20
|
+
const alt = (0, utils_1.getAttr)(node, "alt");
|
|
21
|
+
if (alt === undefined) {
|
|
22
|
+
issues.push({
|
|
23
|
+
ruleId: "img-alt",
|
|
24
|
+
message: "Image elements must have an alt attribute.",
|
|
25
|
+
filePath: context.filePath,
|
|
26
|
+
line: (_a = node.loc) === null || _a === void 0 ? void 0 : _a.line,
|
|
27
|
+
column: (_b = node.loc) === null || _b === void 0 ? void 0 : _b.column,
|
|
28
|
+
severity: "error"
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (alt === "") {
|
|
33
|
+
const linkAncestor = ancestors.find((ancestor) => ancestor.tagName === "a");
|
|
34
|
+
if (linkAncestor && !hasAccessibleName(linkAncestor)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
ruleId: "img-alt",
|
|
37
|
+
message: "Linked images with empty alt text need another accessible name.",
|
|
38
|
+
filePath: context.filePath,
|
|
39
|
+
line: (_c = node.loc) === null || _c === void 0 ? void 0 : _c.line,
|
|
40
|
+
column: (_d = node.loc) === null || _d === void 0 ? void 0 : _d.column,
|
|
41
|
+
severity: "warn"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return issues;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRuleById = exports.builtInRules = void 0;
|
|
4
|
+
const imgAlt_1 = require("./imgAlt");
|
|
5
|
+
const clickableNonsemantic_1 = require("./clickableNonsemantic");
|
|
6
|
+
const routerLinkText_1 = require("./routerLinkText");
|
|
7
|
+
const iconOnlyControl_1 = require("./iconOnlyControl");
|
|
8
|
+
const contrastRatio_1 = require("./contrastRatio");
|
|
9
|
+
exports.builtInRules = [
|
|
10
|
+
imgAlt_1.imgAltRule,
|
|
11
|
+
clickableNonsemantic_1.clickableNonSemanticRule,
|
|
12
|
+
routerLinkText_1.routerLinkTextRule,
|
|
13
|
+
iconOnlyControl_1.iconOnlyControlRule,
|
|
14
|
+
contrastRatio_1.contrastRatioRule
|
|
15
|
+
];
|
|
16
|
+
const getRuleById = (id) => {
|
|
17
|
+
return exports.builtInRules.find((rule) => rule.id === id);
|
|
18
|
+
};
|
|
19
|
+
exports.getRuleById = getRuleById;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.routerLinkTextRule = void 0;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
exports.routerLinkTextRule = {
|
|
6
|
+
id: "routerlink-text",
|
|
7
|
+
description: "Router links need an accessible name.",
|
|
8
|
+
severity: "error",
|
|
9
|
+
check: (ast, context) => {
|
|
10
|
+
const issues = [];
|
|
11
|
+
(0, utils_1.walkWithAncestors)(ast, (node) => {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
if (node.type !== "element" || node.tagName !== "a")
|
|
14
|
+
return;
|
|
15
|
+
if (!(0, utils_1.hasAttr)(node, "routerLink") && !(0, utils_1.hasAttr)(node, "[routerLink]"))
|
|
16
|
+
return;
|
|
17
|
+
const ariaLabel = (0, utils_1.getAttr)(node, "aria-label");
|
|
18
|
+
const text = (0, utils_1.getAccessibleText)(node).replace(/\s+/g, " ").trim();
|
|
19
|
+
if (!(typeof ariaLabel === "string" && ariaLabel.trim().length > 0) && text.length === 0) {
|
|
20
|
+
issues.push({
|
|
21
|
+
ruleId: "routerlink-text",
|
|
22
|
+
message: "Router links need text or aria-label for an accessible name.",
|
|
23
|
+
filePath: context.filePath,
|
|
24
|
+
line: (_a = node.loc) === null || _a === void 0 ? void 0 : _a.line,
|
|
25
|
+
column: (_b = node.loc) === null || _b === void 0 ? void 0 : _b.column,
|
|
26
|
+
severity: "error"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return issues;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ElementNode, Node, RootNode } from "../parser/ast";
|
|
2
|
+
export declare const getAttr: (node: ElementNode, name: string) => string | true | undefined;
|
|
3
|
+
export declare const hasAttr: (node: ElementNode, name: string) => boolean;
|
|
4
|
+
export declare const getTextContent: (node: RootNode | Node) => string;
|
|
5
|
+
export declare const getAccessibleText: (node: RootNode | Node) => string;
|
|
6
|
+
export declare const walkWithAncestors: (node: RootNode | Node, visit: (node: Node, ancestors: ElementNode[]) => void, ancestors?: ElementNode[]) => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.walkWithAncestors = exports.getAccessibleText = exports.getTextContent = exports.hasAttr = exports.getAttr = void 0;
|
|
4
|
+
const getAttr = (node, name) => {
|
|
5
|
+
return node.attrs[name];
|
|
6
|
+
};
|
|
7
|
+
exports.getAttr = getAttr;
|
|
8
|
+
const hasAttr = (node, name) => {
|
|
9
|
+
return Object.prototype.hasOwnProperty.call(node.attrs, name);
|
|
10
|
+
};
|
|
11
|
+
exports.hasAttr = hasAttr;
|
|
12
|
+
const getTextContent = (node) => {
|
|
13
|
+
if (node.type === "root") {
|
|
14
|
+
return node.children.map(exports.getTextContent).join("");
|
|
15
|
+
}
|
|
16
|
+
if (node.type === "text") {
|
|
17
|
+
return node.value;
|
|
18
|
+
}
|
|
19
|
+
return node.children.map(exports.getTextContent).join("");
|
|
20
|
+
};
|
|
21
|
+
exports.getTextContent = getTextContent;
|
|
22
|
+
const iconTags = new Set(["i", "svg", "mat-icon", "fa-icon"]);
|
|
23
|
+
const getAccessibleText = (node) => {
|
|
24
|
+
if (node.type === "root") {
|
|
25
|
+
return node.children.map(exports.getAccessibleText).join("");
|
|
26
|
+
}
|
|
27
|
+
if (node.type === "text") {
|
|
28
|
+
return node.value;
|
|
29
|
+
}
|
|
30
|
+
if (iconTags.has(node.tagName)) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return node.children.map(exports.getAccessibleText).join("");
|
|
34
|
+
};
|
|
35
|
+
exports.getAccessibleText = getAccessibleText;
|
|
36
|
+
const walkWithAncestors = (node, visit, ancestors = []) => {
|
|
37
|
+
if (node.type === "root") {
|
|
38
|
+
for (const child of node.children) {
|
|
39
|
+
(0, exports.walkWithAncestors)(child, visit, ancestors);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
visit(node, ancestors);
|
|
44
|
+
if (node.type === "element") {
|
|
45
|
+
const nextAncestors = [...ancestors, node];
|
|
46
|
+
for (const child of node.children) {
|
|
47
|
+
(0, exports.walkWithAncestors)(child, visit, nextAncestors);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
exports.walkWithAncestors = walkWithAncestors;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readFileContent: (filePath: string) => string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readFileContent = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const readFileContent = (filePath) => {
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(filePath, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.readFileContent = readFileContent;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.fileSystemWalker = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
/**
|
|
40
|
+
* Walk one or more root directories and return all matching file paths.
|
|
41
|
+
*/
|
|
42
|
+
const fileSystemWalker = (rootDirs, options) => {
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const root of rootDirs) {
|
|
45
|
+
if (!fs.existsSync(root))
|
|
46
|
+
continue;
|
|
47
|
+
let stat;
|
|
48
|
+
try {
|
|
49
|
+
stat = fs.statSync(root);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const resolvedPath = path.resolve(root);
|
|
57
|
+
walkDirectory(resolvedPath, options, results);
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
};
|
|
61
|
+
exports.fileSystemWalker = fileSystemWalker;
|
|
62
|
+
/**
|
|
63
|
+
* Recursively walk a directory and collect matching files.
|
|
64
|
+
*/
|
|
65
|
+
const walkDirectory = (currentDir, options, results) => {
|
|
66
|
+
if (shouldIgnorePath(currentDir, options.ignoreDirs))
|
|
67
|
+
return;
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
walkDirectory(fullPath, options, results);
|
|
79
|
+
}
|
|
80
|
+
else if (entry.isFile()) {
|
|
81
|
+
if (matchesPattern(entry.name, options.includePatterns) &&
|
|
82
|
+
!shouldIgnorePath(fullPath, options.ignoreDirs)) {
|
|
83
|
+
results.push(fullPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if the given path should be ignored based on ignoreDirs.
|
|
90
|
+
* ignoreDirs are treated as directory *names* (e.g. "node_modules", "dist").
|
|
91
|
+
*/
|
|
92
|
+
const shouldIgnorePath = (p, ignoreDirs) => {
|
|
93
|
+
const normalized = path.resolve(p);
|
|
94
|
+
const parts = normalized.split(path.sep);
|
|
95
|
+
for (const dirName of ignoreDirs) {
|
|
96
|
+
if (parts.includes(dirName))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Checks whether a file name matches any of the include patterns.
|
|
103
|
+
*
|
|
104
|
+
* Supported pattern forms:
|
|
105
|
+
* "*.component.html" → filename endsWith(".component.html")
|
|
106
|
+
* "*.component.ts" → filename endsWith(".component.ts")
|
|
107
|
+
* "exactName.ext" → filename === pattern
|
|
108
|
+
*/
|
|
109
|
+
const matchesPattern = (fileName, patterns) => {
|
|
110
|
+
for (const pattern of patterns) {
|
|
111
|
+
if (pattern.startsWith("*.")) {
|
|
112
|
+
const suffix = pattern.slice(1);
|
|
113
|
+
if (fileName.endsWith(suffix))
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (fileName === pattern)
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { A11yLensConfig } from "../types/config";
|
|
2
|
+
export interface WalkOptions {
|
|
3
|
+
includePatterns: string[];
|
|
4
|
+
ignoreDirs: string[];
|
|
5
|
+
ignorePatterns: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const fileSystemWalker: (rootDirs: string[], options: WalkOptions) => string[];
|
|
8
|
+
export declare const defaultWalkOptions: (config: A11yLensConfig) => WalkOptions;
|