//------------------------------------------------------------------------------
const esquery = require("esquery");
-const lodash = require("lodash");
//------------------------------------------------------------------------------
// Typedefs
// Helpers
//------------------------------------------------------------------------------
+/**
+ * Computes the union of one or more arrays
+ * @param {...any[]} arrays One or more arrays to union
+ * @returns {any[]} The union of the input arrays
+ */
+function union(...arrays) {
+
+ // TODO(stephenwade): Replace this with arrays.flat() when we drop support for Node v10
+ return [...new Set([].concat(...arrays))];
+}
+
+/**
+ * Computes the intersection of one or more arrays
+ * @param {...any[]} arrays One or more arrays to intersect
+ * @returns {any[]} The intersection of the input arrays
+ */
+function intersection(...arrays) {
+ if (arrays.length === 0) {
+ return [];
+ }
+
+ let result = [...new Set(arrays[0])];
+
+ for (const array of arrays.slice(1)) {
+ result = result.filter(x => array.includes(x));
+ }
+ return result;
+}
+
/**
* Gets the possible types of a selector
* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
if (typesForComponents.every(Boolean)) {
- return lodash.union(...typesForComponents);
+ return union(...typesForComponents);
}
return null;
}
* If at least one of the components could only match a particular type, the compound could only match
* the intersection of those types.
*/
- return lodash.intersection(...typesForComponents);
+ return intersection(...typesForComponents);
}
case "child":
try {
return esquery.parse(rawSelector.replace(/:exit$/u, ""));
} catch (err) {
- if (typeof err.offset === "number") {
- throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
+ if (err.location && err.location.start && typeof err.location.start.offset === "number") {
+ throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`);
}
throw err;
}
}
+const selectorCache = new Map();
+
/**
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
* @param {string} rawSelector A raw AST selector
* @returns {ASTSelector} A selector descriptor
*/
-const parseSelector = lodash.memoize(rawSelector => {
+function parseSelector(rawSelector) {
+ if (selectorCache.has(rawSelector)) {
+ return selectorCache.get(rawSelector);
+ }
+
const parsedSelector = tryParseSelector(rawSelector);
- return {
+ const result = {
rawSelector,
isExit: rawSelector.endsWith(":exit"),
parsedSelector,
attributeCount: countClassAttributes(parsedSelector),
identifierCount: countIdentifiers(parsedSelector)
};
-});
+
+ selectorCache.set(rawSelector, result);
+ return result;
+}
//------------------------------------------------------------------------------
// Public Interface
* An SafeEmitter which is the destination of events. This emitter must already
* have registered listeners for all of the events that it needs to listen for.
* (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
+ * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
* @returns {NodeEventGenerator} new instance
*/
- constructor(emitter) {
+ constructor(emitter, esqueryOptions) {
this.emitter = emitter;
+ this.esqueryOptions = esqueryOptions;
this.currentAncestry = [];
this.enterSelectorsByNodeType = new Map();
this.exitSelectorsByNodeType = new Map();
* @returns {void}
*/
applySelector(node, selector) {
- if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) {
+ if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
this.emitter.emit(selector.rawSelector, node);
}
}