UNPKG

@asamuzakjp/nwsapi

Version:

Fast CSS Selectors API Engine

1,461 lines (1,380 loc) 61.5 kB
/** * Forked and modified from nwsapi@2.2.2 * - Export to cjs only * - Remove ./modules directory * - Remove unused exported properties * - Remove unused pseudo-classes * - Remove Snapshot.root and resolve document.documentElement on runtime * - Use `let` and `const` as much as possible * - Use `===` and `!==` * - Fix `:nth-of-type()` * - Fix function source for :root, :target and :indeterminate pseudo-classes * - Fix <ident-token> * - Support complex selectors within `:is()` and `:not()` * - Add ::slotted() and ::part() to pseudo-elements list * - Add isContentEditable() function * - Add createMatchingParensRegex() function from upstream * - Invalidate cache for :has() pseudo class * - Optimize some regular expressions */ /* * Copyright (C) 2007-2019 Diego Perini * All rights reserved. * * nwsapi.js - Fast CSS Selectors API Engine * * Author: Diego Perini <diego.perini at gmail com> * Version: 2.2.0 * Created: 20070722 * Release: 20220901 * * License: * http://javascript.nwbox.com/nwsapi/MIT-LICENSE * Download: * http://javascript.nwbox.com/nwsapi/nwsapi.js */ (function Export(global, factory) { 'use strict'; module.exports = factory; })(this, function Factory(global, Export) { const version = 'nwsapi-2.2.2'; let doc = global.document; /** * Generate a regex that matches a balanced set of parentheses. * Outermost parentheses are excluded so any amount of children can be handled. * See https://stackoverflow.com/a/35271017 for reference * * @param {number} depth * @return {string} */ function createMatchingParensRegex(depth = 1) { const out = '\\([^)(]*?(?:'.repeat(depth) + '\\([^)(]*?\\)' + '[^)(]*?)*?\\)'.repeat(depth); // remove outermost escaped parens return out.slice(2, out.length - 2); } const CFG = { // extensions operators: '[~*^$|]=|=', combinators: '[\\s>+~](?=[^>+~])' }; const NOT = { // not enclosed in double/single/parens/square doubleEnc: '(?=(?:[^"]*"[^"]*")*[^"]*$)', singleEnc: "(?=(?:[^']*'[^']*')*[^']*$)", parensEnc: '(?![^\\x28]*\\x29)', squareEnc: '(?![^\\x5b]*\\x5d)' }; const REX = { // regular expressions hasEscapes: /\\/, hexNumbers: /^[0-9a-f]/i, escOrQuote: /^\\|[\x22\x27]/, regExpChar: /(?:(?!\\)[\\^$.*+?()[\]{}|/])/g, trimSpaces: /[\r\n\f]|^\s+|\s+$/g, commaGroup: RegExp('(\\s{0,255},\\s{0,255})' + NOT.squareEnc + NOT.parensEnc, 'g'), splitGroup: /((?:\x28[^\x29]{0,255}\x29|\[[^\]]{0,255}\]|\\.|[^,])+)/g, fixEscapes: /\\([0-9a-f]{1,6}\s?|.)|([\x22\x27])/gi, combineWSP: RegExp('\\s{1,255}' + NOT.singleEnc + NOT.doubleEnc, 'g'), tabCharWSP: RegExp('(\\s?\\t{1,255}\\s?)' + NOT.singleEnc + NOT.doubleEnc, 'g'), pseudosWSP: RegExp('\\s{1,255}([-+])\\s{1,255}' + NOT.squareEnc, 'g') }; const STD = { combinator: /\s?([>+~])\s?/g, apimethods: /^(?:[a-z]+|\*)\|/i, namespaces: /(\*|[a-z]+)\|[-a-z]+/i }; const GROUPS = { // pseudo-classes requiring parameters logicalsel: '(is|where|matches|not|has)(?:\\x28\\s?(' + createMatchingParensRegex(3) + ')\\s?\\x29)', treestruct: '(nth(?:-last)?(?:-child|-of-type))(?:\\x28\\s?(even|odd|(?:[-+]?\\d*)(?:n\\s?[-+]?\\s?\\d*)?)\\s?(?:\\x29|$))', // pseudo-classes not requiring parameters locationpc: '(any-link|link|visited|target)\\b', structural: '(root|empty|(?:(?:first|last|only)(?:-child|-of-type)))\\b', inputstate: '(enabled|disabled|read-(?:only|write)|placeholder-shown|default)\\b', inputvalue: '(checked|indeterminate)\\b', // pseudo-classes for parsing only selectors pseudoNop: '(autofill|-webkit-autofill)\\b', // pseudo-elements starting with single colon (:) pseudoSng: '(after|before|first-letter|first-line)\\b', // pseudo-elements starting with double colon (::) pseudoDbl: ':(after|before|first-letter|first-line|selection|part|placeholder|slotted|-webkit-[-a-z0-9]{2,})\\b' }; const Patterns = { // pseudo-classes treestruct: RegExp('^:(?:' + GROUPS.treestruct + ')(.*)', 'i'), structural: RegExp('^:(?:' + GROUPS.structural + ')(.*)', 'i'), inputstate: RegExp('^:(?:' + GROUPS.inputstate + ')(.*)', 'i'), inputvalue: RegExp('^:(?:' + GROUPS.inputvalue + ')(.*)', 'i'), locationpc: RegExp('^:(?:' + GROUPS.locationpc + ')(.*)', 'i'), logicalsel: RegExp('^:(?:' + GROUPS.logicalsel + ')(.*)', 'i'), pseudoNop: RegExp('^:(?:' + GROUPS.pseudoNop + ')(.*)', 'i'), pseudoSng: RegExp('^:(?:' + GROUPS.pseudoSng + ')(.*)', 'i'), pseudoDbl: RegExp('^:(?:' + GROUPS.pseudoDbl + ')(.*)', 'i'), // combinator symbols children: /^\s?>\s?(.*)/, adjacent: /^\s?\+\s?(.*)/, relative: /^\s?~\s?(.*)/, ancestor: /^\s+(.*)/, // universal & namespace universal: /^\*(.*)/, namespace: /^(\w+|\*)?\|(.*)/ }; // emulate firefox error strings const qsNotArgs = 'Not enough arguments'; const qsInvalid = ' is not a valid selector'; // detect structural pseudo-classes in selectors const reNthElem = /(:nth(?:-last)?-child)/i; const reNthType = /(:nth(?:-last)?-of-type)/i; // placeholder for global regexp let reOptimizer; let reValidator; // special handling configuration flags const Config = { IDS_DUPES: true, MIXEDCASE: true, LOGERRORS: true, VERBOSITY: true }; let NAMESPACE; let QUIRKS_MODE; let HTML_DOCUMENT; const ATTR_STD_OPS = { '=': 1, '^=': 1, '$=': 1, '|=': 1, '*=': 1, '~=': 1 }; const HTML_TABLE = { accept: 1, 'accept-charset': 1, align: 1, alink: 1, axis: 1, bgcolor: 1, charset: 1, checked: 1, clear: 1, codetype: 1, color: 1, compact: 1, declare: 1, defer: 1, dir: 1, direction: 1, disabled: 1, enctype: 1, face: 1, frame: 1, hreflang: 1, 'http-equiv': 1, lang: 1, language: 1, link: 1, media: 1, method: 1, multiple: 1, nohref: 1, noresize: 1, noshade: 1, nowrap: 1, readonly: 1, rel: 1, rev: 1, rules: 1, scope: 1, scrolling: 1, selected: 1, shape: 1, target: 1, text: 1, type: 1, valign: 1, valuetype: 1, vlink: 1 }; const Combinators = {}; const Selectors = {}; const Operators = { '=': { p1: '^', p2: '$', p3: 'true' }, '^=': { p1: '^', p2: '', p3: 'true' }, '$=': { p1: '', p2: '$', p3: 'true' }, '*=': { p1: '', p2: '', p3: 'true' }, '|=': { p1: '^', p2: '(-|$)', p3: 'true' }, '~=': { p1: '(^|\\s)', p2: '(\\s|$)', p3: 'true' } }; const concatCall = function (nodes, callback) { let i = 0; const l = nodes.length; const list = Array(l); while (l > i) { if (callback(list[i] = nodes[i]) === false) { break; } ++i; } return list; }; const concatList = function (list, nodes) { let i = -1; let l = nodes.length; while (l--) { list[list.length] = nodes[++i]; } return list; }; let hasDupes = false; const documentOrder = function (a, b) { if (!hasDupes && a === b) { hasDupes = true; return 0; } return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; const unique = function (nodes) { let i = 0; let j = -1; let l = nodes.length + 1; const list = []; while (--l) { if (nodes[i++] === nodes[i]) { continue; } list[++j] = nodes[i - 1]; } hasDupes = false; return list; }; // check context for mixed content const hasMixedCaseTagNames = function (context) { const api = 'getElementsByTagNameNS'; // current host context (ownerDocument) context = context.ownerDocument || context; // documentElement (root) element namespace or default html/xhtml namespace const ns = context.documentElement && context.documentElement.namespaceURI ? context.documentElement.namespaceURI : 'http://www.w3.org/1999/xhtml'; // checking the number of non HTML nodes in the document return (context[api]('*', '*').length - context[api](ns, '*').length) > 0; }; // check if the document type is HTML const isHTML = function (node) { const doc = node.ownerDocument || node; return doc.nodeType === 9 && doc.contentType === 'text/html'; }; // convert single codepoint to UTF-16 encoding const codePointToUTF16 = function (codePoint) { // out of range, use replacement character if (codePoint < 1 || codePoint > 0x10ffff || (codePoint > 0xd7ff && codePoint < 0xe000)) { return '\\ufffd'; } // javascript strings are UTF-16 encoded if (codePoint < 0x10000) { const lowHex = '000' + codePoint.toString(16); return '\\u' + lowHex.substr(lowHex.length - 4); } // supplementary high + low surrogates return '\\u' + (((codePoint - 0x10000) >> 0x0a) + 0xd800).toString(16) + '\\u' + (((codePoint - 0x10000) % 0x400) + 0xdc00).toString(16); }; // convert single codepoint to string const stringFromCodePoint = function (codePoint) { // out of range, use replacement character if (codePoint < 1 || codePoint > 0x10ffff || (codePoint > 0xd7ff && codePoint < 0xe000)) { return '\ufffd'; } if (codePoint < 0x10000) { return String.fromCharCode(codePoint); } return String.fromCodePoint(codePoint); }; // convert escape sequence in a CSS string or identifier // to javascript string with javascript escape sequences const convertEscapes = function (str) { return REX.hasEscapes.test(str) ? str.replace(REX.fixEscapes, function (substring, p1, p2) { // unescaped " or ' return p2 ? '\\' + p2 // javascript strings are UTF-16 encoded : REX.hexNumbers.test(p1) ? codePointToUTF16(parseInt(p1, 16)) // \' \" : REX.escOrQuote.test(p1) ? substring // \g \h \. \# etc : p1; }) : str; }; // convert escape sequence in a CSS string or identifier // to javascript string with characters representations const unescapeIdentifier = function (str) { return REX.hasEscapes.test(str) ? str.replace(REX.fixEscapes, function (substring, p1, p2) { // unescaped " or ' return p2 || (REX.hexNumbers.test(p1) ? stringFromCodePoint(parseInt(p1, 16)) // \' \" : REX.escOrQuote.test(p1) ? substring // \g \h \. \# etc : p1); }) : str; }; // empty set const none = []; // cached lambdas const matchLambdas = {}; const selectLambdas = {}; // cached resolvers let matchResolvers = {}; let selectResolvers = {}; const method = { '#': 'getElementById', '*': 'getElementsByTagName', '|': 'getElementsByTagNameNS', '.': 'getElementsByClassName' }; // find duplicate ids using iterative walk const byIdRaw = function (id, context) { let node = context; const nodes = []; let next = node.firstElementChild; while ((node = next)) { node.id === id && nodes.push(node); if ((next = node.firstElementChild || node.nextElementSibling)) { continue; } while (!next && (node = node.parentElement) && node !== context) { next = node.nextElementSibling; } } return nodes; }; // context agnostic getElementById const byId = function (id, context) { let e; const api = method['#']; // duplicates id allowed if (Config.IDS_DUPES === false) { if (api in context) { e = context[api](id); return e ? [e] : none; } } else if ('all' in context) { if ((e = context.all[id])) { if (e.nodeType === 1) { return e.getAttribute('id') !== id ? [] : [e]; } else if (id === 'length') { e = context[api](id); return e ? [e] : none; } const nodes = []; for (let i = 0, l = e.length; l > i; ++i) { if (e[i].id === id) { nodes.push(e[i]); } } return nodes.length ? nodes : none; } else { return none; } } return byIdRaw(id, context); }; // context agnostic getElementsByTagName const byTag = function (tag, context) { let e; let nodes; const api = method['*']; // DOCUMENT_NODE (9) & ELEMENT_NODE (1) if (api in context) { return Array.prototype.slice.call(context[api](tag)); } else { tag = tag.toLowerCase(); // DOCUMENT_FRAGMENT_NODE (11) if ((e = context.firstElementChild)) { if (!(e.nextElementSibling || tag === '*' || e.localName === tag)) { return Array.prototype.slice.call(e[api](tag)); } else { nodes = []; do { if (tag === '*' || e.localName === tag) { nodes.push(e); } concatList(nodes, e[api](tag)); } while ((e = e.nextElementSibling)); } } else { nodes = none; } } return nodes; }; // context agnostic getElementsByClassName const byClass = function (cls, context) { let e; let nodes; const api = method['.']; let reCls; // DOCUMENT_NODE (9) & ELEMENT_NODE (1) if (api in context) { return Array.prototype.slice.call(context[api](cls)); } else { // DOCUMENT_FRAGMENT_NODE (11) if ((e = context.firstElementChild)) { reCls = RegExp('(^|\\s)' + cls + '(\\s|$)', QUIRKS_MODE ? 'i' : ''); if (!(e.nextElementSibling || reCls.test(e.className))) { return Array.prototype.slice.call(e[api](cls)); } else { nodes = []; do { if (reCls.test(e.className)) { nodes.push(e); } concatList(nodes, e[api](cls)); } while ((e = e.nextElementSibling)); } } else nodes = none; } return nodes; }; const compat = { '#': function (c, n) { REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); return function (e, f) { return byId(n, c); }; }, '*': function (c, n) { REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); return function (e, f) { return byTag(n, c); }; }, '|': function (c, n) { REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); return function (e, f) { return byTag(n, c); }; }, '.': function (c, n) { REX.hasEscapes.test(n) && (n = unescapeIdentifier(n)); return function (e, f) { return byClass(n, c); }; } }; // namespace aware hasAttribute // helper for XML/XHTML documents const hasAttributeNS = function (e, name) { let i; let l; const attr = e.getAttributeNames(); name = RegExp(':?' + name + '$', HTML_DOCUMENT ? 'i' : ''); for (i = 0, l = attr.length; l > i; ++i) { if (name.test(attr[i])) { return true; } } return false; }; // fast resolver for the :nth-child() and :nth-last-child() pseudo-classes const nthElement = (function () { let idx = 0; let len = 0; let set = 0; let parent; let parents = []; let nodes = []; return function (element, dir) { // ensure caches are emptied after each run, invoking with dir = 2 if (dir === 2) { idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; return -1; } let e, i, j, k, l; if (parent === element.parentElement) { i = set; j = idx; l = len; } else { l = parents.length; parent = element.parentElement; for (i = -1, j = 0, k = l - 1; l > j; ++j, --k) { if (parents[j] === parent) { i = j; break; } if (parents[k] === parent) { i = k; break; } } if (i < 0) { parents[i = l] = parent; l = 0; nodes[i] = []; e = (parent && parent.firstElementChild) || element; while (e) { nodes[i][l] = e; if (e === element) { j = l; } e = e.nextElementSibling; ++l; } set = i; idx = 0; len = l; if (l < 2) { return l; } } else { l = nodes[i].length; set = i; } } if (element !== nodes[i][j] && element !== nodes[i][j = 0]) { for (j = 0, e = nodes[i], k = l - 1; l > j; ++j, --k) { if (e[j] === element) { break; } if (e[k] === element) { j = k; break; } } } idx = j + 1; len = l; return dir ? l - j : idx; }; })(); // fast resolver for the :nth-of-type() and :nth-last-of-type() pseudo-classes const nthOfType = (function () { let idx = 0; let len = 0; let set = 0; let parent; let parents = []; let nodes = []; return function (element, dir) { // ensure caches are emptied after each run, invoking with dir = 2 if (dir === 2) { idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; return -1; } const name = element.localName; const nsURI = element.namespaceURI; if (nsURI !== 'http://www.w3.org/1999/xhtml') { idx = 0; len = 0; set = 0; nodes = []; parents = []; parent = undefined; } let e; let i; let j; let k; let l; if (nodes[set] && nodes[set][name] && parent === element.parentElement) { i = set; j = idx; l = len; } else { l = parents.length; parent = element.parentElement; for (i = -1, j = 0, k = l - 1; l > j; ++j, --k) { if (parents[j] === parent) { i = j; break; } if (parents[k] === parent) { i = k; break; } } if (i < 0 || !nodes[i][name]) { parents[i = l] = parent; nodes[i] || (nodes[i] = Object()); l = 0; nodes[i][name] = []; e = (parent && parent.firstElementChild) || element; while (e) { if (e === element) { j = l; } if (e.localName === name && e.namespaceURI === nsURI) { nodes[i][name][l] = e; ++l; } e = e.nextElementSibling; } set = i; idx = j; len = l; if (l < 2) { return l; } } else { l = nodes[i][name].length; set = i; } } if (element !== nodes[i][name][j] && element !== nodes[i][name][j = 0]) { for (j = 0, e = nodes[i][name], k = l - 1; l > j; ++j, --k) { if (e[j] === element) { break; } if (e[k] === element) { j = k; break; } } } idx = j + 1; len = l; return dir ? l - j : idx; }; })(); // check if the node is the target const isTarget = function (node) { const doc = node.ownerDocument || node; const { hash } = new URL(doc.URL); if (node.id && hash === `#${node.id}` && doc.contains(node)) { return true; } return false; }; // check if node is indeterminate const isIndeterminate = function (node) { if ((node.indeterminate && node.localName === 'input' && node.type === 'checkbox') || (node.localName === 'progress' && !node.hasAttribute('value'))) { return true; } if (node.localName === 'input' && node.type === 'radio' && !node.hasAttribute('checked')) { const nodeName = node.name; let parent = node.parentNode; while (parent) { if (parent.localName === 'form') { break; } parent = parent.parentNode; } if (!parent) { const doc = node.ownerDocument; parent = doc.documentElement; } const items = parent.getElementsByTagName('input'); const l = items.length; let checked; for (let i = 0; i < l; i++) { const item = items[i]; if (item.getAttribute('type') === 'radio') { if (nodeName) { if (item.getAttribute('name') === nodeName) { checked = !!item.checked; } } else if (!item.hasAttribute('name')) { checked = !!item.checked; } if (checked) { break; } } } if (!checked) { return true; } } return false; }; // check if node content is editable const isContentEditable = function (node) { let attrValue = 'inherit'; if (node.hasAttribute('contenteditable')) { attrValue = node.getAttribute('contenteditable'); } switch (attrValue) { case '': case 'plaintext-only': case 'true': return true; case 'false': return false; default: if (node.parentNode && node.parentNode.nodeType === 1) { return isContentEditable(node.parentNode); } return false; } }; // build validation regexps used by the engine const setIdentifierSyntax = function () { // // NOTE: SPECIAL CASES IN CSS SYNTAX PARSING RULES // // The <EOF-token> https://drafts.csswg.org/css-syntax/#typedef-eof-token // allow mangled|unclosed selector syntax at the end of selectors strings // // Literal equivalent hex representations of the characters: " ' ` ] ) // // \\x22 = " - double quotes \\x5b = [ - open square bracket // \\x27 = ' - single quote \\x5d = ] - closed square bracket // \\x60 = ` - back tick \\x28 = ( - open round parens // \\x5c = \ - back slash \\x29 = ) - closed round parens // // using hex format prevents false matches of opened/closed instances // pairs, coloring breakage and other editors highlightning problems. // // @see https://drafts.csswg.org/css-syntax-3/#ident-token-diagram const nonascii = '[^\\x00-\\x9f]'; const esctoken = '\\\\(?:[^\\r\\n\\f\\da-f]|[\\da-f]{1,6}\\s{0,255})'; const identifier = '(?:--|-?(?:[a-z_]|' + nonascii + '|' + esctoken + '))' + '(?:[\\w-]|' + nonascii + '|' + esctoken + ')*'; const pseudonames = '[-\\w]+'; const pseudoparms = '(?:[-+]?\\d*)(?:n\\s?[-+]?\\s?\\d*)'; const doublequote = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*(?:"|$)'; const singlequote = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*(?:'|$)"; const attrparser = identifier + '|' + doublequote + '|' + singlequote; const attrvalues = '([\\x22\\x27]?)((?!\\3)*|(?:\\\\?.)*?)(?:\\3|$)'; const attributes = '\\[' + // attribute presence '(?:\\*\\|)?\\s?(' + identifier + '(?::' + identifier + ')?)\\s?' + '(?:(' + CFG.operators + ')\\s?(?:' + attrparser + '))?' + // attribute case sensitivity '(?:\\s?\\b(i))?\\s?' + '(?:\\]|$)'; const attrmatcher = attributes.replace(attrparser, attrvalues); const pseudoclass = '(?:\\x28\\s*' + '(?:' + pseudoparms + '?)?|' + // universal * & // namespace *|* '[*|]|' + '(?:' + '(?::' + pseudonames + '(?:\\x28' + pseudoparms + '?(?:\\x29|$))?)|' + '(?:[.#]?' + identifier + ')|' + '(?:' + attributes + ')' + ')+|' + '\\s?[>+~]\\s?|' + '\\s?,\\s?|' + '\\s|' + '\\x29|$' + ')*'; const standardValidator = '(?=\\s?[^>+~(){}<])' + '(?:' + // universal * & // namespace *|* '\\*|\\||' + '(?:[.#]?' + identifier + ')+|' + '(?:' + attributes + ')+|' + '(?:::?' + pseudonames + pseudoclass + ')|' + '(?:\\s?' + CFG.combinators + '\\s?)|' + '\\s?,\\s?|' + '\\s?' + ')+'; // the following global RE is used to return the // deepest localName in selector strings and then // use it to retrieve all possible matching nodes // that will be filtered by compiled resolvers reOptimizer = RegExp( '(?:([.:#*]?)(' + identifier + ')' + '(?::[-\\w]+|\\[[^\\]]+(?:\\]|$)|\\x28[^\\x29]+(?:\\x29|$))*' + ')$', 'i'); // global reValidator = RegExp(standardValidator, 'gi'); Patterns.id = RegExp('^#(' + identifier + ')(.*)', 'i'); Patterns.tagName = RegExp('^(' + identifier + ')(.*)', 'i'); Patterns.className = RegExp('^\\.(' + identifier + ')(.*)', 'i'); Patterns.attribute = RegExp('^(?:' + attrmatcher + ')(.*)'); }; // configure the engine to use special handling const configure = function (option, clear) { if (typeof option === 'string') { return !!Config[option]; } if (typeof option !== 'object') { return Config; } for (const i in option) { Config[i] = !!option[i]; } // clear lambda cache if (clear) { matchResolvers = {}; selectResolvers = {}; } setIdentifierSyntax(); return true; }; // centralized error and exceptions handling const emit = function (message, proto) { let err; if (Config.VERBOSITY) { if (global[proto]) { err = new global[proto](message); } else { err = new global.DOMException(message, 'SyntaxError'); } throw err; } if (Config.LOGERRORS && console && console.log) { console.log(message); } }; // passed to resolvers const Snapshot = { doc: null, from: null, byTag: null, first: null, match: null, ancestor: null, nthOfType: null, nthElement: null, hasAttributeNS: null, isTarget: null, isIndeterminate: null, isContentEditable: null }; // context let lastContext; const switchContext = function (context, force) { const oldDoc = doc; doc = context.ownerDocument || context; if (force || oldDoc !== doc) { // force a new check for each document change // performed before the next select operation HTML_DOCUMENT = isHTML(doc); QUIRKS_MODE = HTML_DOCUMENT && doc.compatMode.indexOf('CSS') < 0; NAMESPACE = doc.documentElement && doc.documentElement.namespaceURI; Snapshot.doc = doc; } Snapshot.from = context; return context; }; // selector let lastMatched; let lastSelected; const F_INIT = '"use strict";return function Resolver(c,f,x,r)'; const S_HEAD = 'var e,n,o,j=r.length-1,k=-1'; const M_HEAD = 'var e,n,o'; const S_LOOP = 'main:while((e=c[++k]))'; const N_LOOP = 'main:while((e=c.item(++k)))'; const M_LOOP = 'e=c;'; const S_BODY = 'r[++j]=c[k];'; const N_BODY = 'r[++j]=c.item(k);'; const M_BODY = ''; const S_TAIL = 'continue main;'; const M_TAIL = 'r=true;'; const S_TEST = 'if(f(c[k])){break main;}'; const N_TEST = 'if(f(c.item(k))){break main;}'; const M_TEST = 'f(c);'; let S_VARS = []; let M_VARS = []; // build conditional code to check components of selector strings const compileSelector = function (expression, source, mode, callback) { // N is the negation pseudo-class flag // D is the default inverted negation flag let a; let b; let n; let f; let name; let NS; const N = ''; const D = '!'; let compat; let expr; let match; let result; let status; let symbol; let test; let type; let selector = expression; let vars; // original 'select' or 'match' selector string before normalization const selectorString = mode ? lastSelected : lastMatched; // isolate selector combinators/components and normalize whitespace selector = selector.replace(STD.combinator, '$1'); // .replace(STD.whitespace, ' '); let selectorRecursion = true; while (selector) { // get namespace prefix if present or get first char of selector symbol = STD.apimethods.test(selector) ? '|' : selector[0]; switch (symbol) { // universal resolver case '*': match = selector.match(Patterns.universal); if (N === '!') { source = 'if(' + N + 'true' + '){' + source + '}'; } break; // id resolver case '#': match = selector.match(Patterns.id); source = 'if(' + N + '(/^' + match[1] + '$/.test(e.getAttribute("id"))' + ')){' + source + '}'; break; // class name resolver case '.': match = selector.match(Patterns.className); compat = (QUIRKS_MODE ? 'i' : '') + '.test(e.getAttribute("class"))'; source = 'if(' + N + '(/(^|\\s)' + match[1] + '(\\s|$)/' + compat + ')){' + source + '}'; break; // tag name resolver case (/[_a-z]/i.test(symbol) ? symbol : undefined): match = selector.match(Patterns.tagName); source = 'if(' + N + '(e.localName' + (Config.MIXEDCASE || hasMixedCaseTagNames(doc) ? '=="' + match[1].toLowerCase() + '"' : '=="' + match[1].toUpperCase() + '"') + ')){' + source + '}'; break; // namespace resolver case '|': match = selector.match(Patterns.namespace); if (match[1] === '*') { source = 'if(' + N + 'true){' + source + '}'; } else if (!match[1]) { source = 'if(' + N + '(!e.namespaceURI)){' + source + '}'; } else if (typeof match[1] === 'string' && doc.documentElement && doc.documentElement.prefix === match[1]) { source = 'if(' + N + '(e.namespaceURI=="' + NAMESPACE + '")){' + source + '}'; } else { emit('\'' + selectorString + '\'' + qsInvalid); } break; // attributes resolver case '[': match = selector.match(Patterns.attribute); NS = match[0].match(STD.namespaces); name = match[1]; expr = name.split(':'); expr = expr.length === 2 ? expr[1] : expr[0]; if (match[2] && !(test = Operators[match[2]])) { emit('\'' + selectorString + '\'' + qsInvalid); return ''; } if (match[4] === '') { test = match[2] === '~=' ? { p1: '^\\s', p2: '+$', p3: 'true' } : match[2] in ATTR_STD_OPS && match[2] !== '~=' ? { p1: '^', p2: '$', p3: 'true' } : test; } else if (match[2] === '~=' && match[4].includes(' ')) { // whitespace separated list but value contains space source = 'if(' + N + 'false){' + source + '}'; break; } else if (match[4]) { match[4] = convertEscapes(match[4]).replace(REX.regExpChar, '\\$&'); } type = match[5] === 'i' || (HTML_DOCUMENT && HTML_TABLE[expr.toLowerCase()]) ? 'i' : ''; source = 'if(' + N + '(' + (!match[2] ? (NS ? 's.hasAttributeNS(e,"' + name + '")' : 'e.hasAttribute&&e.hasAttribute("' + name + '")') : !match[4] && ATTR_STD_OPS[match[2]] && match[2] !== '~=' ? 'e.getAttribute&&e.getAttribute("' + name + '")==""' : '(/' + test.p1 + match[4] + test.p2 + '/' + type + ').test(e.getAttribute&&e.getAttribute("' + name + '"))==' + test.p3) + ')){' + source + '}'; break; // *** General sibling combinator // E ~ F (F relative sibling of E) case '~': match = selector.match(Patterns.relative); source = 'n=e;while((e=e.previousElementSibling)){' + source + '}e=n;'; break; // *** Adjacent sibling combinator // E + F (F adiacent sibling of E) case '+': match = selector.match(Patterns.adjacent); source = 'n=e;if((e=e.previousElementSibling)){' + source + '}e=n;'; break; // *** Descendant combinator // E F (E ancestor of F) case '\x09': case '\x20': match = selector.match(Patterns.ancestor); source = 'n=e;while((e=e.parentElement)){' + source + '}e=n;'; break; // *** Child combinator // E > F (F children of E) case '>': match = selector.match(Patterns.children); source = 'n=e;if((e=e.parentElement)){' + source + '}e=n;'; break; // *** user supplied combinators extensions case (symbol in Combinators ? symbol : undefined): // for other registered combinators extensions match[match.length - 1] = '*'; source = Combinators[symbol](match) + source; break; // *** tree-structural pseudo-classes // :root, :empty, :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type case ':': if ((match = selector.match(Patterns.structural))) { match[1] = match[1].toLowerCase(); switch (match[1]) { case 'root': // there can only be one :root element, so exit the loop once found source = 'if(' + N + '(e===s.doc.documentElement)){' + source + (mode ? 'break main;' : '') + '}'; break; case 'empty': // matches elements that don't contain elements or text nodes source = 'n=e.firstChild;while(n&&!(/1|3/).test(n.nodeType)){n=n.nextSibling}if(' + D + 'n){' + source + '}'; break; // *** child-indexed pseudo-classes // :first-child, :last-child, :only-child case 'only-child': source = 'if(' + N + '(!e.nextElementSibling&&!e.previousElementSibling)){' + source + '}'; break; case 'last-child': source = 'if(' + N + '(!e.nextElementSibling)){' + source + '}'; break; case 'first-child': source = 'if(' + N + '(!e.previousElementSibling)){' + source + '}'; break; // *** typed child-indexed pseudo-classes // :only-of-type, :last-of-type, :first-of-type case 'only-of-type': source = 'o=e.localName;' + 'n=e;while((n=n.nextElementSibling)&&n.localName!=o);if(!n){' + 'n=e;while((n=n.previousElementSibling)&&n.localName!=o);}if(' + D + 'n){' + source + '}'; break; case 'last-of-type': source = 'n=e;o=e.localName;while((n=n.nextElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}'; break; case 'first-of-type': source = 'n=e;o=e.localName;while((n=n.previousElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}'; break; default: emit('\'' + selectorString + '\'' + qsInvalid); } // *** child-indexed & typed child-indexed pseudo-classes // :nth-child, :nth-of-type, :nth-last-child, :nth-last-of-type } else if ((match = selector.match(Patterns.treestruct))) { match[1] = match[1].toLowerCase(); switch (match[1]) { case 'nth-child': case 'nth-of-type': case 'nth-last-child': case 'nth-last-of-type': expr = /-of-type/i.test(match[1]); if (match[1] && match[2]) { type = /last/i.test(match[1]); if (match[2] === 'n') { source = 'if(' + N + 'true){' + source + '}'; break; } else if (match[2] === '1') { test = type ? 'next' : 'previous'; source = expr ? 'n=e;o=e.localName;' + 'while((n=n.' + test + 'ElementSibling)&&n.localName!=o);if(' + D + 'n){' + source + '}' : 'if(' + N + '!e.' + test + 'ElementSibling){' + source + '}'; break; } else if (match[2] === 'even' || match[2] === '2n0' || match[2] === '2n+0' || match[2] === '2n') { test = 'n%2==0'; } else if (match[2] === 'odd' || match[2] === '2n1' || match[2] === '2n+1') { test = 'n%2==1'; } else { f = /n/i.test(match[2]); n = match[2].split('n'); a = parseInt(n[0], 10) || 0; b = parseInt(n[1], 10) || 0; if (n[0] === '-') { a = -1; } if (n[0] === '+') { a = +1; } test = (b ? '(n' + (b > 0 ? '-' : '+') + Math.abs(b) + ')' : 'n') + '%' + a + '==0'; test = a >= +1 ? (f ? 'n>' + (b - 1) + (Math.abs(a) !== 1 ? '&&' + test : '') : 'n==' + a) : a <= -1 ? (f ? 'n<' + (b + 1) + (Math.abs(a) !== 1 ? '&&' + test : '') : 'n==' + a) : a === 0 ? (n[0] ? 'n==' + b : 'n>' + (b - 1)) : 'false'; } expr = expr ? 'OfType' : 'Element'; type = type ? 'true' : 'false'; source = 'n=s.nth' + expr + '(e,' + type + ');if(' + N + '(' + test + ')){' + source + '}'; } else { emit('\'' + selectorString + '\'' + qsInvalid); } break; default: emit('\'' + selectorString + '\'' + qsInvalid); } // *** logical combination pseudo-classes // :is( s1, [ s2, ... ]), :not( s1, [ s2, ... ]) } else if ((match = selector.match(Patterns.logicalsel))) { match[1] = match[1].toLowerCase(); expr = match[2].replace(REX.CommaGroup, ',').replace(REX.TrimSpaces, ''); switch (match[1]) { // FIXME: case 'is': case 'where': case 'matches': source = 'if(s.match("' + expr.replace(/\x22/g, '\\"') + '",e)){' + source + '}'; break; // FIXME: case 'not': source = 'if(!s.match("' + expr.replace(/\x22/g, '\\"') + '",e)){' + source + '}'; break; // FIXME: case 'has': // clear cache matchResolvers = {}; source = 'if(e.querySelector(":scope ' + expr.replace(/\x22/g, '\\"') + '")){' + source + '}'; break; default: emit('\'' + selectorString + '\'' + qsInvalid); } // *** location pseudo-classes // :any-link, :link, :visited, :target } else if ((match = selector.match(Patterns.locationpc))) { match[1] = match[1].toLowerCase(); switch (match[1]) { case 'any-link': source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href")||e.visited)){' + source + '}'; break; case 'link': source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href"))){' + source + '}'; break; // FIXME: case 'visited': source = 'if(' + N + '(/^a|area$/i.test(e.localName)&&e.hasAttribute("href")&&e.visited)){' + source + '}'; break; case 'target': source = 'if(s.isTarget(e)){' + source + '}'; break; default: emit('\'' + selectorString + '\'' + qsInvalid); } // *** user interface and form pseudo-classes // :enabled, :disabled, :read-only, :read-write, :placeholder-shown, :default } else if ((match = selector.match(Patterns.inputstate))) { match[1] = match[1].toLowerCase(); switch (match[1]) { // FIXME: lacks custom element support case 'enabled': source = 'if((("form" in e||/^optgroup$/i.test(e.localName))&&"disabled" in e &&e.disabled===false' + ')){' + source + '}'; break; // FIXME: lacks custom element support case 'disabled': // https://html.spec.whatwg.org/#enabling-and-disabling-form-controls:-the-disabled-attribute source = 'if((("form" in e||/^optgroup$/i.test(e.localName))&&"disabled" in e)){' + // F is true if any of the fieldset elements in the ancestry chain has the disabled attribute specified // L is true if the first legend element of the fieldset contains the element 'var x=0,N=[],F=false,L=false;' + 'if(!(/^(optgroup|option)$/i.test(e.localName))){' + 'n=e.parentElement;' + 'while(n){' + 'if(n.localName==="fieldset"){' + 'N[x++]=n;' + 'if(n.disabled===true){' + 'F=true;' + 'break;' + '}' + '}' + 'n=n.parentElement;' + '}' + 'for(var x=0;x<N.length;x++){' + 'if((n=s.first("legend",N[x]))&&n.contains(e)){' + 'L=true;' + 'break;' + '}' + '}' + '}' + 'if(e.disabled===true||(F&&!L)){' + source + '}}'; break; case 'read-only': source = 'if(' + '(/^textarea$/i.test(e.localName)&&(e.readOnly||e.disabled))||' + '(/^input$/i.test(e.localName)&&("|date|datetime-local|email|month|number|password|search|tel|text|time|url|week|".includes("|"+e.type+"|")?(e.readOnly||e.disabled):true))||' + '(!/^(?:input|textarea)$/i.test(e.localName) && !s.isContentEditable(e))' + '){' + source + '}'; break; case 'read-write': source = 'if(' + '(/^textarea$/i.test(e.localName)&&!e.readOnly&&!e.disabled)||' + '(/^input$/i.test(e.localName)&&"|date|datetime-local|email|month|number|password|search|tel|text|time|url|week|".includes("|"+e.type+"|")&&!e.readOnly&&!e.disabled)||' + '(!/^(?:input|textarea)$/i.test(e.localName) && s.isContentEditable(e))' + '){' + source + '}'; break; // FIXME: case 'placeholder-shown': source = 'if((' + '(/^input|textarea$/i.test(e.localName))&&e.hasAttribute("placeholder")&&' + '("|textarea|password|number|search|email|text|tel|url|".includes("|"+e.type+"|"))&&' + '(!s.match(":focus",e))' + ')){' + source + '}'; break; // FIXME: case 'default': source = 'if(("form" in e && e.form)){' + 'var x=0;n=[];' + 'if(e.type=="image")n=e.form.getElementsByTagName("input");' + 'if(e.type=="submit")n=e.form.elements;' + 'while(n[x]&&e!==n[x]){' + 'if(n[x].type=="image")break;' + 'if(n[x].type=="submit")break;' + 'x++;' + '}' + '}' + 'if((e.form&&(e===n[x]&&"|image|submit|".includes("|"+e.type+"|"))||' + '((/^option$/i.test(e.localName))&&e.defaultSelected)||' + '(("|radio|checkbox|".includes("|"+e.type+"|"))&&e.defaultChecked)' + ')){' + source + '}'; break; default: emit('\'' + selector_string + '\'' + qsInvalid); break; } // *** input pseudo-classes (for form validation) // :checked, :indeterminate, :valid, :invalid, :in-range, :out-of-range, :required, :optional } else if ((match = selector.match(Patterns.inputvalue))) { match[1] = match[1].toLowerCase(); switch (match[1]) { case 'checked': source = 'if(' + N + '(/^input$/i.test(e.localName)&&' + '("|radio|checkbox|".includes("|"+e.type+"|")&&e.checked)||' + '(/^option$/i.test(e.localName)&&(e.selected||e.checked))' + ')){' + source + '}'; break; case 'indeterminate': source = 'if(s.isIndeterminate(e)){' + source + '}'; break; // FIXME: case 'required': source = 'if(' + N + '(/^input|select|textarea$/i.test(e.localName)&&e.required)' + '){' + source + '}'; break; // FIXME: case 'optional': source = 'if(' + N + '(/^input|select|textarea$/i.test(e.localName)&&!e.required)' + '){' + source + '}'; break; // FIXME: case 'invalid': source = 'if(' + N + '((' + '(/^form$/i.test(e.localName)&&!e.noValidate)||' + '(e.willValidate&&!e.formNoValidate))&&!e.checkValidity())||' + '(/^fieldset$/i.test(e.localName)&&s.first(":invalid",e))' + '){' + source + '}'; break; // FIXME: case 'valid': source = 'if(' + N + '((' + '(/^form$/i.test(e.localName)&&!e.noValidate)||' + '(e.willValidate&&!e.formNoValidate))&&e.checkValidity())||' + '(/^fieldset$/i.test(e.localName)&&s.first(":valid",e))' + '){' + source + '}'; break; // FIXME: case 'in-range': source = 'if(' + N + '(/^input$/i.test(e.localName))&&' + '(e.willValidate&&!e.formNoValidate)&&' + '(!e.validity.rangeUnderflow&&!e.validity.rangeOverflow)&&' + '("|date|datetime-local|month|number|range|time|week|".includes("|"+e.type+"|"))&&' + '("range"==e.type||e.getAttribute("min")||e.getAttribute("max"))' + '){' + source + '}'; break; // FIXME: case 'out-of-range': source = 'if(' + N + '(/^input$/i.test(e.localName))&&' + '(e.willValidate&&!e.formNoValidate)&&' + '(e.validity.rangeUnderflow||e.validity.rangeOverflow)&&' + '("|date|datetime-local|month|number|range|time|week|".includes("|"+e.type+"|"))&&' + '("range"==e.type||e.getAttribute("min")||e.getAttribute("max"))' + '){' + source + '}'; break; default: emit('\'' + selectorString + '\'' + qsInvalid); } // allow pseudo-elements starting with single colon (:) // :after, :before, :first-letter, :first-line // assert: e.type is in double-colon format, like ::after } else if ((match = selector.match(Patterns.pseudoSng))) { source = 'if(e.element&&e.type.toLowerCase()=="' + ':' + match[0].toLowerCase() + '"){e=e.element;' + source + '}'; // allow pseudo-elements starting with double colon (::) // ::after, ::before, ::marker, ::placeholder, ::inactive-selection, ::selection, ::-webkit-<foo-bar> // assert: e.type is in double-colon format, like ::after } else if ((match = selector.match(Patterns.pseudoDbl))) { source = 'if(e.element&&e.type.toLowerCase()=="' + match[0].toLowerCase() + '"){e=e.element;' + source + '}'; // placeholder for parsed only no-op selectors } else if ((match = selector.match(Patterns.pseudoNop))) { source = 'if(' + N + 'false' + '){' + source + '}'; } else { // reset expr = false; status = false; // process registered selector extensions for (expr in Selectors) { if ((match = selector.match(Selectors[expr].Expression))) { result = Selectors[expr].Callback(match, source, mode, callback); if ('match' in result) { match = result.match; } vars = result.modvar; if (mode) { // add extra select() vars vars && !S_VARS.includes(vars) && S_VARS.push(vars); } else { // add extra match() vars vars && M_VARS.includes