Spaces:
Paused
Paused
| /** | |
| * 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(vars) && M_VARS.push(vars); | |
| } | |
| // extension source code | |
| source = result.source; | |
| // extension status code | |
| status = result.status; | |
| // break on status error | |
| if (status) { break; } | |
| } | |
| } | |
| if (!status) { | |
| emit('unknown pseudo-class selector \'' + selector + '\''); | |
| return ''; | |
| } | |
| if (!expr) { | |
| emit('unknown token in selector \'' + selector + '\''); | |
| return ''; | |
| } | |
| } | |
| break; | |
| default: | |
| selectorRecursion = false; | |
| emit('\'' + selectorString + '\'' + qsInvalid); | |
| } | |
| // end of switch symbol | |
| if (!selectorRecursion) { | |
| break; | |
| } | |
| if (!match) { | |
| emit('\'' + selectorString + '\'' + qsInvalid); | |
| return ''; | |
| } | |
| // pop last component | |
| selector = match.pop(); | |
| } | |
| // end of while selector | |
| return source; | |
| }; | |
| // compile groups or single selector strings into | |
| // executable functions for matching or selecting | |
| const compile = function (selector, mode, callback) { | |
| let head = ''; let loop = ''; let macro = ''; let source = ''; let vars = ''; | |
| // 'mode' can be boolean or null | |
| // true = select / false = match | |
| // null to use collection.item() | |
| switch (mode) { | |
| case true: | |
| if (selectLambdas[selector]) { | |
| return selectLambdas[selector]; | |
| } | |
| macro = S_BODY + (callback ? S_TEST : '') + S_TAIL; | |
| head = S_HEAD; | |
| loop = S_LOOP; | |
| break; | |
| case false: | |
| if (matchLambdas[selector]) { | |
| return matchLambdas[selector]; | |
| } | |
| macro = M_BODY + (callback ? M_TEST : '') + M_TAIL; | |
| head = M_HEAD; | |
| loop = M_LOOP; | |
| break; | |
| case null: | |
| if (selectLambdas[selector]) { | |
| return selectLambdas[selector]; | |
| } | |
| macro = N_BODY + (callback ? N_TEST : '') + S_TAIL; | |
| head = S_HEAD; | |
| loop = N_LOOP; | |
| break; | |
| default: | |
| } | |
| source = compileSelector(selector, macro, mode, callback); | |
| loop += (mode || mode === null) ? '{' + source + '}' : source; | |
| if ((mode || mode === null) && selector.includes(':nth')) { | |
| loop += reNthElem.test(selector) ? 's.nthElement(null, 2);' : ''; | |
| loop += reNthType.test(selector) ? 's.nthOfType(null, 2);' : ''; | |
| } | |
| if (S_VARS[0] || M_VARS[0]) { | |
| vars = ',' + (S_VARS.join(',') || M_VARS.join(',')); | |
| S_VARS = []; | |
| M_VARS = []; | |
| } | |
| const factory = Function('s', F_INIT + '{' + head + vars + ';' + loop + 'return r;}')(Snapshot); | |
| return mode || mode === null ? (selectLambdas[selector] = factory) : (matchLambdas[selector] = factory); | |
| }; | |
| // optimize selectors avoiding duplicated checks | |
| const optimize = function (selector, token) { | |
| const index = token.index; | |
| const length = token[1].length + token[2].length; | |
| return selector.slice(0, index) + | |
| (' >+~'.indexOf(selector.charAt(index - 1)) > -1 | |
| ? (':['.indexOf(selector.charAt(index + length + 1)) > -1 | |
| ? '*' | |
| : '') | |
| : '') + selector.slice(index + length - (token[1] === '*' ? 1 : 0)); | |
| }; | |
| // prepare factory resolvers and closure collections | |
| const collect = function (selectors, context, callback) { | |
| let i; | |
| let l; | |
| const seen = { }; | |
| let token = ['', '*', '*']; | |
| const optimized = selectors; | |
| const factory = []; | |
| const htmlset = []; | |
| const nodeset = []; | |
| let results = []; | |
| let type; | |
| for (i = 0, l = selectors.length; l > i; ++i) { | |
| if (!seen[selectors[i]] && (seen[selectors[i]] = true)) { | |
| type = selectors[i].match(reOptimizer); | |
| if (type && type[1] !== ':' && (token = type)) { | |
| token[1] || (token[1] = '*'); | |
| optimized[i] = optimize(optimized[i], token); | |
| } else { | |
| token = ['', '*', '*']; | |
| } | |
| } | |
| nodeset[i] = token[1] + token[2]; | |
| htmlset[i] = compat[token[1]](context, token[2]); | |
| factory[i] = compile(optimized[i], true, null); | |
| factory[i] | |
| ? factory[i](htmlset[i](), callback, context, results) | |
| : results.concat(htmlset[i]()); | |
| } | |
| if (l > 1) { | |
| results.sort(documentOrder); | |
| hasDupes && (results = unique(results)); | |
| } | |
| return { | |
| callback, | |
| context, | |
| factory, | |
| htmlset, | |
| nodeset, | |
| results | |
| }; | |
| }; | |
| // replace ':scope' pseudo-class with element references | |
| const makeref = function (selectors, element) { | |
| // DOCUMENT_NODE (9) | |
| if (element.nodeType === 9) { | |
| element = element.documentElement; | |
| } | |
| return selectors.replace(/:scope/gi, | |
| element.localName + | |
| (element.id ? '#' + element.id : '') + | |
| (element.className ? '.' + element.classList[0] : '')); | |
| }; | |
| const matchAssert = function (f, element, callback) { | |
| let r = false; | |
| for (let i = 0, l = f.length; l > i; ++i) { | |
| f[i](element, callback, null, false) && (r = true); | |
| } | |
| return r; | |
| }; | |
| const matchCollect = function (selectors, callback) { | |
| const f = []; | |
| for (let i = 0, l = selectors.length; l > i; ++i) { | |
| f[i] = compile(selectors[i], false, callback); | |
| } | |
| return { factory: f }; | |
| }; | |
| // equivalent of w3c 'matches' method | |
| const match = function _matches(selectors, element, callback) { | |
| let expressions; | |
| if (element && !/:has\(/.test(selectors) && matchResolvers[selectors]) { | |
| return matchAssert(matchResolvers[selectors].factory, element, callback); | |
| } | |
| lastMatched = selectors; | |
| // arguments validation | |
| if (arguments.length === 0) { | |
| emit(qsNotArgs, 'TypeError'); | |
| return Config.VERBOSITY ? undefined : false; | |
| } else if (arguments[0] === '') { | |
| emit('\'\'' + qsInvalid); | |
| return Config.VERBOSITY ? undefined : false; | |
| } | |
| // input NULL or UNDEFINED | |
| if (typeof selectors !== 'string') { | |
| selectors = '' + selectors; | |
| } | |
| if ((/:scope/i).test(selectors)) { | |
| selectors = makeref(selectors, element); | |
| } | |
| // normalize input string | |
| const parsed = selectors | |
| .replace(/\0|\\$/g, '\ufffd') | |
| .replace(REX.combineWSP, '\x20') | |
| .replace(REX.pseudosWSP, '$1') | |
| .replace(REX.tabCharWSP, '\t') | |
| .replace(REX.commaGroup, ',') | |
| .replace(REX.trimSpaces, ''); | |
| // parse, validate and split possible compound selectors | |
| if ((expressions = parsed.match(reValidator)) && expressions.join('') === parsed) { | |
| expressions = parsed.match(REX.splitGroup); | |
| if (parsed[parsed.length - 1] === ',') { | |
| emit(qsInvalid); | |
| return Config.VERBOSITY ? undefined : false; | |
| } | |
| } else { | |
| emit('\'' + selectors + '\'' + qsInvalid); | |
| return Config.VERBOSITY ? undefined : false; | |
| } | |
| matchResolvers[selectors] = matchCollect(expressions, callback); | |
| return matchAssert(matchResolvers[selectors].factory, element, callback); | |
| }; | |
| // equivalent of w3c 'closest' method | |
| const ancestor = function _closest(selectors, element, callback) { | |
| if ((/:scope/i).test(selectors)) { | |
| selectors = makeref(selectors, element); | |
| } | |
| while (element) { | |
| if (match(selectors, element, callback)) break; | |
| element = element.parentElement; | |
| } | |
| return element; | |
| }; | |
| // equivalent of w3c 'querySelectorAll' method | |
| const select = function _querySelectorAll(selectors, context, callback) { | |
| let expressions; let nodes = []; let resolver; | |
| context || (context = doc); | |
| if (selectors) { | |
| if ((resolver = selectResolvers[selectors])) { | |
| if (resolver.context === context && resolver.callback === callback) { | |
| const f = resolver.factory; | |
| const h = resolver.htmlset; | |
| const n = resolver.nodeset; | |
| if (n.length > 1) { | |
| const l = n.length; | |
| for (let i = 0, l = n.length, list; l > i; ++i) { | |
| list = compat[n[i][0]](context, n[i].slice(1))(); | |
| if (f[i] !== null) { | |
| f[i](list, callback, context, nodes); | |
| } else { | |
| nodes = nodes.concat(list); | |
| } | |
| } | |
| if (l > 1 && nodes.length > 1) { | |
| nodes.sort(documentOrder); | |
| hasDupes && (nodes = unique(nodes)); | |
| } | |
| } else { | |
| if (f[0]) { | |
| nodes = f[0](h[0](), callback, context, nodes); | |
| } else { | |
| nodes = h[0](); | |
| } | |
| } | |
| return typeof callback === 'function' | |
| ? concatCall(nodes, callback) | |
| : nodes; | |
| } | |
| } | |
| } | |
| lastSelected = selectors; | |
| // arguments validation | |
| if (arguments.length === 0) { | |
| emit(qsNotArgs, 'TypeError'); | |
| return Config.VERBOSITY ? undefined : none; | |
| } else if (arguments[0] === '') { | |
| emit('\'\'' + qsInvalid); | |
| return Config.VERBOSITY ? undefined : none; | |
| } else if (lastContext !== context) { | |
| lastContext = switchContext(context); | |
| } | |
| // input NULL or UNDEFINED | |
| if (typeof selectors !== 'string') { | |
| selectors = '' + selectors; | |
| } | |
| if ((/:scope/i).test(selectors)) { | |
| selectors = makeref(selectors, context); | |
| } | |
| // normalize input string | |
| const parsed = selectors | |
| .replace(/\0|\\$/g, '\ufffd') | |
| .replace(REX.combineWSP, '\x20') | |
| .replace(REX.pseudosWSP, '$1') | |
| .replace(REX.tabCharWSP, '\t') | |
| .replace(REX.commaGroup, ',') | |
| .replace(REX.trimSpaces, ''); | |
| // parse, validate and split possible compound selectors | |
| if ((expressions = parsed.match(reValidator)) && expressions.join('') === parsed) { | |
| expressions = parsed.match(REX.splitGroup); | |
| if (parsed[parsed.length - 1] === ',') { | |
| emit(qsInvalid); | |
| return Config.VERBOSITY ? undefined : false; | |
| } | |
| } else { | |
| emit('\'' + selectors + '\'' + qsInvalid); | |
| return Config.VERBOSITY ? undefined : false; | |
| } | |
| // save/reuse factory and closure collection | |
| selectResolvers[selectors] = collect(expressions, context, callback); | |
| nodes = selectResolvers[selectors].results; | |
| return typeof callback === 'function' | |
| ? concatCall(nodes, callback) | |
| : nodes; | |
| }; | |
| // equivalent of w3c 'querySelector' method | |
| const first = function _querySelector(selectors, context, callback) { | |
| if (arguments.length === 0) { | |
| emit(qsNotArgs, 'TypeError'); | |
| } | |
| return select(selectors, context, typeof callback === 'function' | |
| ? function firstMatch(element) { | |
| callback(element); | |
| return false; | |
| } | |
| : function firstMatch() { | |
| return false; | |
| } | |
| )[0] || null; | |
| }; | |
| // execute the engine initialization code | |
| const initialize = function (d) { | |
| setIdentifierSyntax(); | |
| lastContext = switchContext(d, true); | |
| Snapshot.doc = doc; | |
| Snapshot.from = doc; | |
| Snapshot.byTag = byTag; | |
| Snapshot.first = first; | |
| Snapshot.match = match; | |
| Snapshot.ancestor = ancestor; | |
| Snapshot.nthOfType = nthOfType; | |
| Snapshot.nthElement = nthElement; | |
| Snapshot.hasAttributeNS = hasAttributeNS; | |
| Snapshot.isTarget = isTarget; | |
| Snapshot.isIndeterminate = isIndeterminate; | |
| Snapshot.isContentEditable = isContentEditable; | |
| }; | |
| initialize(doc); | |
| // public exported methods/objects | |
| const Dom = { | |
| // exported engine methods | |
| Version: version, | |
| configure, | |
| match, | |
| closest: ancestor, | |
| first, | |
| select | |
| }; | |
| return Dom; | |
| }); | |