/** * # Optimize * * 1.) Improve efficiency through shorter selectors by removing redundancy * 2.) Improve robustness through selector transformation */ import adapt from './adapt' import { convertNodeList } from './utilities' /** * Apply different optimization techniques * * @param {string} selector - [description] * @param {HTMLElement|Array.} element - [description] * @param {Object} options - [description] * @return {string} - [description] */ export default function optimize (selector, elements, options = {}) { // convert single entry and NodeList if (!Array.isArray(elements)) { elements = !elements.length ? [elements] : convertNodeList(elements) } if (!elements.length || elements.some((element) => element.nodeType !== 1)) { throw new Error(`Invalid input - to compare HTMLElements its necessary to provide a reference of the selected node(s)! (missing "elements")`) } const globalModified = adapt(elements[0], options) // chunk parts outside of quotes (http://stackoverflow.com/a/25663729) var path = selector.replace(/> /g, '>').split(/\s+(?=(?:(?:[^"]*"){2})*[^"]*$)/) if (path.length < 2) { return optimizePart('', selector, '', elements) } const shortened = [path.pop()] while (path.length > 1) { const current = path.pop() const prePart = path.join(' ') const postPart = shortened.join(' ') const pattern = `${prePart} ${postPart}` const matches = document.querySelectorAll(pattern) if (matches.length !== elements.length) { shortened.unshift(optimizePart(prePart, current, postPart, elements)) } } shortened.unshift(path[0]) path = shortened // optimize start + end path[0] = optimizePart('', path[0], path.slice(1).join(' '), elements) path[path.length-1] = optimizePart(path.slice(0, -1).join(' '), path[path.length-1], '', elements) if (globalModified) { delete global.document } return path.join(' ').replace(/>/g, '> ').trim() } /** * Improve a chunk of the selector * * @param {string} prePart - [description] * @param {string} current - [description] * @param {string} postPart - [description] * @param {Array.} elements - [description] * @return {string} - [description] */ function optimizePart (prePart, current, postPart, elements) { if (prePart.length) prePart = `${prePart} ` if (postPart.length) postPart = ` ${postPart}` // robustness: attribute without value (generalization) if (/\[*\]/.test(current)) { const key = current.replace(/=.*$/, ']') var pattern = `${prePart}${key}${postPart}` var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = key } else { // robustness: replace specific key-value with base tag (heuristic) const references = document.querySelectorAll(`${prePart}${key}`) for (var i = 0, l = references.length; i < l; i++) { const reference = references[i] if (elements.some((element) => reference.contains(element))) { const description = reference.tagName.toLowerCase() var pattern = `${prePart}${description}${postPart}` var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = description } break } } } } // robustness: descendant instead child (heuristic) if (/>/.test(current)) { const descendant = current.replace(/>/, '') var pattern = `${prePart}${descendant}${postPart}` var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = descendant } } // robustness: 'nth-of-type' instead 'nth-child' (heuristic) if (/:nth-child/.test(current)) { // TODO: consider complete coverage of 'nth-of-type' replacement const type = current.replace(/nth-child/g, 'nth-of-type') var pattern = `${prePart}${type}${postPart}` var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = type } } // efficiency: combinations of classname (partial permutations) if (/\.\S+\.\S+/.test(current)) { var names = current.trim().split('.').slice(1) .map((name) => `.${name}`) .sort((curr, next) => curr.length - next.length) while (names.length) { const partial = current.replace(names.shift(), '').trim() var pattern = `${prePart}${partial}${postPart}`.trim() if (!pattern.length || pattern.charAt(0) === '>' || pattern.charAt(pattern.length-1) === '>') { break } var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = partial } } // robustness: degrade complex classname (heuristic) names = current && current.match(/\./g) if (names && names.length > 2) { const references = document.querySelectorAll(`${prePart}${current}`) for (var i = 0, l = references.length; i < l; i++) { const reference = references[i] if (elements.some((element) => reference.contains(element) )) { // TODO: // - check using attributes + regard excludes const description = reference.tagName.toLowerCase() var pattern = `${prePart}${description}${postPart}` var matches = document.querySelectorAll(pattern) if (compareResults(matches, elements)) { current = description } break } } } } return current } /** * Evaluate matches with expected elements * * @param {Array.} matches - [description] * @param {Array.} elements - [description] * @return {Boolean} - [description] */ function compareResults (matches, elements) { const { length } = matches return length === elements.length && elements.every((element) => { for (var i = 0; i < length; i++) { if (matches[i] === element) { return true } } return false }) }