axe-core
Version:
Accessibility engine for automated Web UI testing
264 lines (239 loc) • 6.67 kB
JavaScript
import cssParser from './css-parser';
function matchesTag(vNode, exp) {
return (
vNode.props.nodeType === 1 &&
(exp.tag === '*' || vNode.props.nodeName === exp.tag)
);
}
function matchesClasses(vNode, exp) {
return !exp.classes || exp.classes.every(cl => vNode.hasClass(cl.value));
}
function matchesAttributes(vNode, exp) {
return (
!exp.attributes ||
exp.attributes.every(att => {
var nodeAtt = vNode.attr(att.key);
return nodeAtt !== null && (!att.value || att.test(nodeAtt));
})
);
}
function matchesId(vNode, exp) {
return !exp.id || vNode.props.id === exp.id;
}
function matchesPseudos(target, exp) {
if (
!exp.pseudos ||
exp.pseudos.every(pseudo => {
if (pseudo.name === 'not') {
return !pseudo.expressions.some(expression => {
return matchesExpression(target, expression);
});
} else if (pseudo.name === 'is') {
return pseudo.expressions.some(expression => {
return matchesExpression(target, expression);
});
}
throw new Error(
'the pseudo selector ' + pseudo.name + ' has not yet been implemented'
);
})
) {
return true;
}
return false;
}
function matchExpression(vNode, expression) {
return (
matchesTag(vNode, expression) &&
matchesClasses(vNode, expression) &&
matchesAttributes(vNode, expression) &&
matchesId(vNode, expression) &&
matchesPseudos(vNode, expression)
);
}
var escapeRegExp = (() => {
/*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan <http://stevenlevithan.com/regex/xregexp/> MIT License */
var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g;
var to = '\\';
return string => {
return string.replace(from, to);
};
})();
const reUnescape = /\\/g;
function convertAttributes(atts) {
/*! Credit Mootools Copyright Mootools, MIT License */
if (!atts) {
return;
}
return atts.map(att => {
const attributeKey = att.name.replace(reUnescape, '');
const attributeValue = (att.value || '').replace(reUnescape, '');
let test, regexp;
switch (att.operator) {
case '^=':
regexp = new RegExp('^' + escapeRegExp(attributeValue));
break;
case '$=':
regexp = new RegExp(escapeRegExp(attributeValue) + '$');
break;
case '~=':
regexp = new RegExp(
'(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)'
);
break;
case '|=':
regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)');
break;
case '=':
test = value => {
return attributeValue === value;
};
break;
case '*=':
test = value => {
return value && value.includes(attributeValue);
};
break;
case '!=':
test = value => {
return attributeValue !== value;
};
break;
default:
test = value => {
return !!value;
};
}
if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) {
test = () => {
return false;
};
}
if (!test) {
test = value => {
return value && regexp.test(value);
};
}
return {
key: attributeKey,
value: attributeValue,
test: test
};
});
}
function convertClasses(classes) {
if (!classes) {
return;
}
return classes.map(className => {
className = className.replace(reUnescape, '');
return {
value: className,
regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)')
};
});
}
function convertPseudos(pseudos) {
if (!pseudos) {
return;
}
return pseudos.map(p => {
var expressions;
if (['is', 'not'].includes(p.name)) {
expressions = p.value;
expressions = expressions.selectors
? expressions.selectors
: [expressions];
expressions = convertExpressions(expressions);
}
return {
name: p.name,
expressions: expressions,
value: p.value
};
});
}
/**
* convert the css-selector-parser format into the Slick format
* @private
* @param Array {Object} expressions
* @return Array {Object}
*
*/
function convertExpressions(expressions) {
return expressions.map(exp => {
var newExp = [];
var rule = exp.rule;
while (rule) {
/* eslint no-restricted-syntax: 0 */
// `.tagName` is a property coming from the `CSSSelectorParser` library
newExp.push({
tag: rule.tagName ? rule.tagName.toLowerCase() : '*',
combinator: rule.nestingOperator ? rule.nestingOperator : ' ',
id: rule.id,
attributes: convertAttributes(rule.attrs),
classes: convertClasses(rule.classNames),
pseudos: convertPseudos(rule.pseudos)
});
rule = rule.rule;
}
return newExp;
});
}
/**
* Convert a CSS selector to the Slick format expression
*
* @private
* @param {String} selector CSS selector to convert
* @returns {Object[]} Array of Slick format expressions
*/
export function convertSelector(selector) {
var expressions = cssParser.parse(selector);
expressions = expressions.selectors ? expressions.selectors : [expressions];
return convertExpressions(expressions);
}
/**
* Determine if a virtual node matches a Slick format CSS expression
*
* @private
* @method matchesExpression
* @memberof axe.utils
* @param {VirtualNode} vNode VirtualNode to match
* @param {Object|Object[]} expressions CSS selector expression or array of expressions
* @returns {Boolean}
*/
export function matchesExpression(vNode, expressions, matchAnyParent) {
const exps = [].concat(expressions);
const expression = exps.pop();
let matches = matchExpression(vNode, expression);
while (!matches && matchAnyParent && vNode.parent) {
vNode = vNode.parent;
matches = matchExpression(vNode, expression);
}
if (exps.length) {
if ([' ', '>'].includes(expression.combinator) === false) {
throw new Error(
'axe.utils.matchesExpression does not support the combinator: ' +
expression.combinator
);
}
matches =
matches &&
matchesExpression(vNode.parent, exps, expression.combinator === ' ');
}
return matches;
}
/**
* matches implementation that operates on a VirtualNode
*
* @method matches
* @memberof axe.utils
* @param {VirtualNode} vNode VirtualNode to match
* @param {String} selector CSS selector string
* @return {Boolean}
*/
function matches(vNode, selector) {
const expressions = convertSelector(selector);
return expressions.some(expression => matchesExpression(vNode, expression));
}
export default matches;