kayle
Version:
Extremely fast and accurate accessibility engine built for any headless tool like playwright or puppeteer.
271 lines • 9.98 kB
JavaScript
((exports) => {
const issueCodeMap = {
unknown: 0,
error: 1,
warning: 2,
notice: 3,
};
const issueCodeReverseMap = {
0: "unknown",
1: "error",
2: "warning",
3: "notice",
};
const A_1 = "color-contrast";
const H_1 = "Principle1.Guideline1_4.1_4_3.G18.Fail";
const A_2 = "has-alt";
const H_2 = "Principle1.Guideline1_1.1_1_1.H37";
const A_3 = "empty-heading";
const H_3 = "Principle1.Guideline1_3.1_3_1.H42.2";
const A_4 = "frame-title";
const H_4 = "Principle2.Guideline2_4.2_4_1.H64.1";
const A_5 = "link-name";
const H_5 = "Principle4.Guideline4_1.4_1_2.H91.A.EmptyNoId";
const A_6 = "heading-order";
const H_6 = "Principle1.Guideline1_3.1_3_1_A.G141";
const scoreMap = new Map([
[A_1, [20, H_1]],
[H_1, [20, A_1]],
[A_2, [20, H_2]],
[H_2, [20, A_2]],
[A_3, [20, H_3]],
[H_3, [20, A_3]],
[A_4, [10, H_4]],
[H_4, [10, A_4]],
[A_5, [20, H_5]],
[H_5, [20, A_5]],
[A_6, [10, H_6]],
[H_6, [10, A_6]],
]);
let rootElement = null;
let hiddenElements = null;
const shapeIssue = (issue, cliped) => {
let context = "";
let selector = "";
let clip;
if (issue.element) {
context = getElementContext(issue.element);
selector = getElementSelector(issue.element);
if (cliped &&
!issue.bounds &&
typeof issue.element.getBoundingClientRect === "function") {
const { x, y, width, height } = issue.element.getBoundingClientRect();
clip = {
x,
y,
width,
height,
};
}
}
const typeCode = issueCodeMap[issue.type ||
(issue.value &&
Array.isArray(issue.value) &&
issue.value.length &&
issue.value[0])];
return {
context: context || issue.snippet,
selector: selector || (issue.path && issue.path.dom ? issue.path.dom : ""),
code: issue.code || issue.ruleId,
type: issue.type || issueCodeReverseMap[typeCode],
typeCode: typeCode || 0,
message: issue.message,
runner: issue.runner ? issue.runner : "kayle",
runnerExtras: issue.runnerExtras,
recurrence: issue.recurrence || 0,
clip: issue.bounds
? {
x: issue.bounds.left || 0,
y: issue.bounds.top || 0,
height: issue.bounds.height || 0,
width: issue.bounds.width || 0,
}
: clip,
};
};
const getElementContext = (element) => {
let outerHTML = element.outerHTML;
if (!outerHTML) {
return "";
}
if (element.innerHTML.length > 31) {
outerHTML = outerHTML.replace(element.innerHTML, `${element.innerHTML.substring(0, 32)}...`);
}
if (outerHTML.length > 251) {
outerHTML = `${outerHTML.substring(0, 251)}...`;
}
return outerHTML;
};
const siblingPosition = (node) => {
let i = 1;
while ((node = node.previousSibling)) {
if (node.nodeType == 1) {
i += 1;
}
}
return i;
};
const getElementSelector = (element, initial) => {
if (!element || element.nodeName == "HTML") {
return "";
}
if (element.id) {
return "#" + element.id;
}
if (element.nodeName == "BODY") {
return "body";
}
const elementSelector = getElementSelector(element.parentNode, true);
return `${!elementSelector && initial ? "html" : elementSelector}>:nth-child(${siblingPosition(element)})`;
};
const runA11y = async (options) => {
const isIssueNotIgnored = (issue) => {
return !options.ignore.some((element) => {
if (element === issue.type) {
return true;
}
const _code = issue.code?.toLowerCase();
if (element === _code) {
return _code;
}
if (element.length >= 8) {
const ignoreSplit = element.split(".");
if (ignoreSplit.length) {
const codeSplit = _code.split(".");
const ruleMatch = ignoreSplit[0] === codeSplit[1] &&
ignoreSplit[1] === codeSplit[2];
if (ignoreSplit.length === 2) {
return true;
}
if (ruleMatch) {
if (ignoreSplit[ignoreSplit.length - 1] ===
codeSplit[codeSplit.length - 1] ||
ignoreSplit[ignoreSplit.length - 1] ===
codeSplit[codeSplit.length - 2]) {
return true;
}
}
}
}
return false;
});
};
const isElementInTestArea = (element) => {
if (!rootElement) {
rootElement = window.document.querySelector(options.rootElement);
}
return rootElement ? rootElement.contains(element) : true;
};
const isElementOutsideHiddenArea = (element) => {
if (!hiddenElements && typeof options.hideElements === "string") {
hiddenElements = window.document.querySelectorAll(options.hideElements);
}
let found = true;
if (hiddenElements && hiddenElements.length) {
found = false;
for (const ele of hiddenElements) {
if (ele.contains(element)) {
found = true;
break;
}
}
}
return found;
};
const validateIssue = (issue) => (options.rootElement && !isElementInTestArea(issue.element)) ||
(options.hideElements && !isElementOutsideHiddenArea(issue.element)) ||
!isIssueNotIgnored(issue) ||
(issue.value &&
Array.isArray(issue.value) &&
issue.value.length >= 2 &&
issue.value[1] === "PASS");
const processIssues = (issues, acc, tracker, meta, missingAltIndexs) => {
for (const is of issues) {
if (validateIssue(is)) {
continue;
}
const issue = shapeIssue(is, options.clip);
const errorType = issue.type === "error";
if (issue.type === "warning") {
meta.warningCount += issue.recurrence + 1;
}
if (issue.type === "notice") {
meta.noticeCount += issue.recurrence + 1;
}
const code = issue.code[0] === "W"
? issue.code.substring(issue.code.indexOf(".") + 1)
: issue.code;
if (scoreMap.has(code)) {
const [accessScore, ref] = scoreMap.get(code);
meta.accessScore -= accessScore;
scoreMap.delete(ref);
scoreMap.delete(code);
}
if (errorType && !tracker.errorPointer) {
acc[tracker.ic] = acc[0];
acc[0] = issue;
}
else if (tracker.errorPointer) {
const right = acc[tracker.errorPointer];
if (right && right.type === "warning" && errorType) {
acc[tracker.ic] = right;
acc[tracker.errorPointer] = issue;
}
else {
acc[tracker.ic] = issue;
}
}
else {
acc[tracker.ic] = issue;
}
if (errorType) {
meta.errorCount += issue.recurrence + 1;
if (issue.code === "Principle1.Guideline1_1.1_1_1.H37" ||
issue.code === "image-alt") {
missingAltIndexs.push(tracker.errorPointer);
}
tracker.errorPointer++;
}
tracker.ic++;
}
};
const runnerIssues = await Promise.all(options.runners.map((runner) => {
return kayle.runners[runner](options, kayle).catch((e) => {
if (e instanceof Error) {
console.error(e);
}
return [];
});
}));
const meta = {
errorCount: 0,
warningCount: 0,
noticeCount: 0,
accessScore: 100,
};
const missingAltIndexs = [];
const issues = new Array(runnerIssues.reduce((r, x) => r + x.length, 0));
const tracker = {
errorPointer: 0,
ic: 0,
};
for (const runnerIssue of runnerIssues) {
processIssues(runnerIssue, issues, tracker, meta, missingAltIndexs);
}
issues.length = tracker.ic;
return {
documentTitle: window.document.title,
pageUrl: window.location.href,
issues,
meta,
automateable: {
missingAltIndexs,
},
};
};
const kayle = (exports.__a11y = {
run: runA11y,
runners: {},
});
})(this);
//# sourceMappingURL=runner.js.map