diff --git a/src/selector-native.js b/src/selector-native.js index 7ca500515..07da6f37a 100644 --- a/src/selector-native.js +++ b/src/selector-native.js @@ -8,13 +8,10 @@ * * Positional selectors (:first; :eq(n); :odd; etc.) * * Type selectors (:input; :checkbox; :button; etc.) * * State-based selectors (:animated; :visible; :hidden; etc.) - * * :has(selector) - * * :not(complex selector) + * * :has(selector) in browsers without native support + * * :not(complex selector) in IE * * custom selectors via jQuery extensions - * * Leading combinators (e.g., $collection.find("> *")) * * Reliable functionality on XML fragments - * * Requiring all parts of a selector to match elements under context - * (e.g., $div.find("div > *") now matches children of $div) * * Matching against non-elements * * Reliable sorting of disconnected nodes * * querySelectorAll bug fixes (e.g., unreliable :focus on WebKit) @@ -26,20 +23,35 @@ import jQuery from "./core.js"; import document from "./var/document.js"; -import documentElement from "./var/documentElement.js"; import whitespace from "./var/whitespace.js"; // The following utils are attached directly to the jQuery object. import "./selector/escapeSelector.js"; import "./selector/uniqueSort.js"; +import isIE from "./var/isIE.js"; +import booleans from "./selector/var/booleans.js"; +import rleadingCombinator from "./selector/var/rleadingCombinator.js"; +import rdescend from "./selector/var/rdescend.js"; +import rsibling from "./selector/var/rsibling.js"; +import matches from "./selector/var/matches.js"; +import testContext from "./selector/testContext.js"; +import filterMatchExpr from "./selector/filterMatchExpr.js"; +import preFilter from "./selector/preFilter.js"; +import tokenize from "./selector/tokenize.js"; +import toSelector from "./selector/toSelector.js"; -// Support: IE 9 - 11+ -// IE requires a prefix. -var matches = documentElement.matches || documentElement.msMatchesSelector; +var matchExpr = jQuery.extend( { + bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + needsContext: new RegExp( "^" + whitespace + "*[>+~]" ) +}, filterMatchExpr ); jQuery.extend( { find: function( selector, context, results, seed ) { - var elem, nodeType, + var elem, nid, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9, i = 0; results = results || []; @@ -51,7 +63,7 @@ jQuery.extend( { } // Early return if context is not an element, document or document fragment - if ( ( nodeType = context.nodeType ) !== 1 && nodeType !== 9 && nodeType !== 11 ) { + if ( nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { return []; } @@ -62,18 +74,65 @@ jQuery.extend( { } } } else { - jQuery.merge( results, context.querySelectorAll( selector ) ); + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && + testContext( context.parentNode ) || + context; + + // Outside of IE, if we're not changing the context we can + // use :scope instead of an ID. + if ( newContext !== context || isIE ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = jQuery.expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + jQuery.merge( results, newContext.querySelectorAll( newSelector ) ); + } finally { + if ( nid === jQuery.expando ) { + context.removeAttribute( "id" ); + } + } } return results; }, expr: { - attrHandle: {}, - match: { - bool: new RegExp( "^(?:checked|selected|async|autofocus|autoplay|controls|defer" + - "|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$", "i" ), - needsContext: new RegExp( "^" + whitespace + "*[>+~]" ) - } + + // Can be adjusted by the user + cacheLength: 50, + + match: matchExpr, + preFilter: preFilter } } ); diff --git a/src/selector.js b/src/selector.js index 117ee3051..328eca45f 100644 --- a/src/selector.js +++ b/src/selector.js @@ -1,7 +1,6 @@ import jQuery from "./core.js"; import nodeName from "./core/nodeName.js"; import document from "./var/document.js"; -import documentElement from "./var/documentElement.js"; import indexOf from "./var/indexOf.js"; import pop from "./var/pop.js"; import push from "./var/push.js"; @@ -9,19 +8,31 @@ import whitespace from "./var/whitespace.js"; import rbuggyQSA from "./selector/rbuggyQSA.js"; import rtrim from "./var/rtrim.js"; import isIE from "./var/isIE.js"; +import identifier from "./selector/var/identifier.js"; +import booleans from "./selector/var/booleans.js"; +import rleadingCombinator from "./selector/var/rleadingCombinator.js"; +import rdescend from "./selector/var/rdescend.js"; +import rsibling from "./selector/var/rsibling.js"; +import matches from "./selector/var/matches.js"; +import createCache from "./selector/createCache.js"; +import testContext from "./selector/testContext.js"; +import filterMatchExpr from "./selector/filterMatchExpr.js"; +import preFilter from "./selector/preFilter.js"; +import selectorError from "./selector/selectorError.js"; +import unescapeSelector from "./selector/unescapeSelector.js"; +import tokenize from "./selector/tokenize.js"; +import toSelector from "./selector/toSelector.js"; import support from "./selector/support.js"; // The following utils are attached directly to the jQuery object. import "./selector/escapeSelector.js"; import "./selector/uniqueSort.js"; -var preferredDoc = document, - matches = documentElement.matches || documentElement.msMatchesSelector; +var preferredDoc = document; ( function() { var i, - Expr, outermostContext, // Local document vars @@ -30,67 +41,20 @@ var i, documentIsHTML, // Instance-specific data - expando = jQuery.expando, dirruns = 0, done = 0, classCache = createCache(), - tokenCache = createCache(), compilerCache = createCache(), nonnativeSelectorCache = createCache(), - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + - "loop|multiple|open|readonly|required|scoped", - // Regular expressions - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rwhitespace = new RegExp( whitespace + "+", "g" ), - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + - whitespace + "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), - matchExpr = { - ID: new RegExp( "^#(" + identifier + ")" ), - CLASS: new RegExp( "^\\.(" + identifier + ")" ), - TAG: new RegExp( "^(" + identifier + "|[*])" ), - ATTR: new RegExp( "^" + attributes ), - PSEUDO: new RegExp( "^" + pseudos ), - CHILD: new RegExp( - "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + matchExpr = jQuery.extend( { bool: new RegExp( "^(?:" + booleans + ")$", "i" ), // For use in libraries implementing .is() @@ -98,7 +62,7 @@ var i, needsContext: new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, + }, filterMatchExpr ), rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, @@ -106,30 +70,6 @@ var i, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - rsibling = /[+~]/, - - // CSS escapes - // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - if ( nonHex ) { - - // Strip the backslash prefix from a non-hex escape sequence - return nonHex; - } - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - return high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - // Used for iframes; see `setDocument`. // Support: IE 9 - 11+ // Removing the function wrapper causes a "Permission Denied" @@ -145,10 +85,6 @@ var i, { dir: "parentNode", next: "legend" } ); -function selectorError( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -} - function find( selector, context, results, seed ) { var m, i, elem, nid, match, groups, newSelector, newContext = context && context.ownerDocument, @@ -223,10 +159,11 @@ function find( selector, context, results, seed ) { // as such selectors are not recognized by querySelectorAll. // Thanks to Andrew Dupont for this technique. if ( nodeType === 1 && - ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + newContext = rsibling.test( selector ) && + testContext( context.parentNode ) || context; // Outside of IE, if we're not changing the context we can @@ -237,7 +174,7 @@ function find( selector, context, results, seed ) { if ( ( nid = context.getAttribute( "id" ) ) ) { nid = jQuery.escapeSelector( nid ); } else { - context.setAttribute( "id", ( nid = expando ) ); + context.setAttribute( "id", ( nid = jQuery.expando ) ); } } @@ -285,7 +222,7 @@ function find( selector, context, results, seed ) { } catch ( qsaError ) { nonnativeSelectorCache( selector, true ); } finally { - if ( nid === expando ) { + if ( nid === jQuery.expando ) { context.removeAttribute( "id" ); } } @@ -297,35 +234,12 @@ function find( selector, context, results, seed ) { return select( selector.replace( rtrim, "$1" ), context, results, seed ); } -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties - // (see https://github.com/jquery/sizzle/issues/157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - /** * Mark a function for special use by jQuery selector module * @param {Function} fn The function to mark */ function markFunction( fn ) { - fn[ expando ] = true; + fn[ jQuery.expando ] = true; return fn; } @@ -427,15 +341,6 @@ function createPositionalPseudo( fn ) { } ); } -/** - * Checks a node for validity as a jQuery selector context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - /** * Sets document-related variables once based on the current document * @param {Element|Object} [node] An element or document object to use to set the document @@ -491,7 +396,7 @@ find.matchesSelector = function( elem, expr ) { return find( expr, document, null, [ elem ] ).length > 0; }; -Expr = jQuery.expr = { +jQuery.expr = { // Can be adjusted by the user cacheLength: 50, @@ -532,99 +437,18 @@ Expr = jQuery.expr = { "~": { dir: "previousSibling" } }, - preFilter: { - ATTR: function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) - .replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - CHILD: function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - selectorError( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) - ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - selectorError( match[ 0 ] ); - } - - return match; - }, - - PSEUDO: function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr.CHILD.test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, + preFilter: preFilter, filter: { ID: function( id ) { - var attrId = id.replace( runescape, funescape ); + var attrId = unescapeSelector( id ); return function( elem ) { return elem.getAttribute( "id" ) === attrId; }; }, TAG: function( nodeNameSelector ) { - var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + var expectedNodeName = unescapeSelector( nodeNameSelector ).toLowerCase(); return nodeNameSelector === "*" ? function() { @@ -739,7 +563,8 @@ Expr = jQuery.expr = { if ( forward && useCache ) { // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + outerCache = parent[ jQuery.expando ] || + ( parent[ jQuery.expando ] = {} ); cache = outerCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex && cache[ 2 ]; @@ -761,7 +586,8 @@ Expr = jQuery.expr = { // Use previously-cached element index if available if ( useCache ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + outerCache = elem[ jQuery.expando ] || + ( elem[ jQuery.expando ] = {} ); cache = outerCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex; @@ -782,8 +608,8 @@ Expr = jQuery.expr = { // Cache the index of each encountered element if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); + outerCache = node[ jQuery.expando ] || + ( node[ jQuery.expando ] = {} ); outerCache[ type ] = [ dirruns, diff ]; } @@ -808,13 +634,14 @@ Expr = jQuery.expr = { // https://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters // Remember that setFilters inherits from pseudos - var fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + var fn = jQuery.expr.pseudos[ pseudo ] || + jQuery.expr.setFilters[ pseudo.toLowerCase() ] || selectorError( "unsupported pseudo: " + pseudo ); // The user may use createPseudo to indicate that // arguments are needed to create the filter function // just as jQuery does - if ( fn[ expando ] ) { + if ( fn[ jQuery.expando ] ) { return fn( argument ); } @@ -834,7 +661,7 @@ Expr = jQuery.expr = { results = [], matcher = compile( selector.replace( rtrim, "$1" ) ); - return matcher[ expando ] ? + return matcher[ jQuery.expando ] ? markFunction( function( seed, matches, _context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), @@ -865,7 +692,7 @@ Expr = jQuery.expr = { } ), contains: markFunction( function( text ) { - text = text.replace( runescape, funescape ); + text = unescapeSelector( text ); return function( elem ) { return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; }; @@ -884,7 +711,7 @@ Expr = jQuery.expr = { if ( !ridentifier.test( lang || "" ) ) { selectorError( "unsupported lang: " + lang ); } - lang = lang.replace( runescape, funescape ).toLowerCase(); + lang = unescapeSelector( lang ).toLowerCase(); return function( elem ) { var elemLang; do { @@ -958,7 +785,7 @@ Expr = jQuery.expr = { }, parent: function( elem ) { - return !Expr.pseudos.empty( elem ); + return !jQuery.expr.pseudos.empty( elem ); }, // Element/input types @@ -1035,102 +862,20 @@ Expr = jQuery.expr = { } }; -Expr.pseudos.nth = Expr.pseudos.eq; +jQuery.expr.pseudos.nth = jQuery.expr.pseudos.eq; // Add button/input type pseudos for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); + jQuery.expr.pseudos[ i ] = createInputPseudo( i ); } for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); + jQuery.expr.pseudos[ i ] = createButtonPseudo( i ); } // Easy API for creating new setFilters function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rcombinators.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - if ( parseOnly ) { - return soFar.length; - } - - return soFar ? - selectorError( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} +setFilters.prototype = jQuery.expr.filters = jQuery.expr.pseudos; +jQuery.expr.setFilters = new setFilters(); function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, @@ -1168,7 +913,7 @@ function addCombinator( matcher, combinator, base ) { } else { while ( ( elem = elem[ dir ] ) ) { if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + outerCache = elem[ jQuery.expando ] || ( elem[ jQuery.expando ] = {} ); if ( skip && nodeName( elem, skip ) ) { elem = elem[ dir ] || elem; @@ -1239,10 +984,10 @@ function condense( unmatched, map, filter, context, xml ) { } function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { + if ( postFilter && !postFilter[ jQuery.expando ] ) { postFilter = setMatcher( postFilter ); } - if ( postFinder && !postFinder[ expando ] ) { + if ( postFinder && !postFinder[ jQuery.expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction( function( seed, results, context, xml ) { @@ -1340,8 +1085,8 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], + leadingRelative = jQuery.expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || jQuery.expr.relative[ " " ], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) @@ -1364,18 +1109,18 @@ function matcherFromTokens( tokens ) { } ]; for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + if ( ( matcher = jQuery.expr.relative[ tokens[ i ].type ] ) ) { matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + matcher = jQuery.expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { + if ( matcher[ jQuery.expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { + if ( jQuery.expr.relative[ tokens[ j ].type ] ) { break; } } @@ -1412,7 +1157,7 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { contextBackup = outermostContext, // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find.TAG( "*", outermost ), + elems = seed || byElement && jQuery.expr.find.TAG( "*", outermost ), // Use integer dirruns iff this is the outermost matcher dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ); @@ -1537,7 +1282,7 @@ function compile( selector, match /* Internal Use Only */ ) { i = match.length; while ( i-- ) { cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { + if ( cached[ jQuery.expando ] ) { setMatchers.push( cached ); } else { elementMatchers.push( cached ); @@ -1577,10 +1322,11 @@ function select( selector, context, results, seed ) { // Reduce context if the leading compound selector is an ID tokens = match[ 0 ] = match[ 0 ].slice( 0 ); if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + context.nodeType === 9 && documentIsHTML && + jQuery.expr.relative[ tokens[ 1 ].type ] ) { - context = ( Expr.find.ID( - token.matches[ 0 ].replace( runescape, funescape ), + context = ( jQuery.expr.find.ID( + unescapeSelector( token.matches[ 0 ] ), context ) || [] )[ 0 ]; if ( !context ) { @@ -1600,14 +1346,14 @@ function select( selector, context, results, seed ) { token = tokens[ i ]; // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { + if ( jQuery.expr.relative[ ( type = token.type ) ] ) { break; } - if ( ( find = Expr.find[ type ] ) ) { + if ( ( find = jQuery.expr.find[ type ] ) ) { // Search, expanding context for leading sibling combinators if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), + unescapeSelector( token.matches[ 0 ] ), rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || context ) ) ) { diff --git a/src/selector/createCache.js b/src/selector/createCache.js new file mode 100644 index 000000000..18e255d0f --- /dev/null +++ b/src/selector/createCache.js @@ -0,0 +1,26 @@ +import jQuery from "../core.js"; + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties + // (see https://github.com/jquery/sizzle/issues/157) + if ( keys.push( key + " " ) > jQuery.expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +export default createCache; diff --git a/src/selector/filterMatchExpr.js b/src/selector/filterMatchExpr.js new file mode 100644 index 000000000..17056a555 --- /dev/null +++ b/src/selector/filterMatchExpr.js @@ -0,0 +1,18 @@ +import whitespace from "../var/whitespace.js"; +import identifier from "./var/identifier.js"; +import attributes from "./var/attributes.js"; +import pseudos from "./var/pseudos.js"; + +var filterMatchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ) +}; + +export default filterMatchExpr; diff --git a/src/selector/preFilter.js b/src/selector/preFilter.js new file mode 100644 index 000000000..4a2fb489a --- /dev/null +++ b/src/selector/preFilter.js @@ -0,0 +1,90 @@ +import rpseudo from "./var/rpseudo.js"; +import filterMatchExpr from "./filterMatchExpr.js"; +import unescapeSelector from "./unescapeSelector.js"; +import selectorError from "./selectorError.js"; +import tokenize from "./tokenize.js"; + +var preFilter = { + ATTR: function( match ) { + match[ 1 ] = unescapeSelector( match[ 1 ] ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = unescapeSelector( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + /* matches from filterMatchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + selectorError( match[ 0 ] ); + } + + // numeric x and y parameters for jQuery.expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + selectorError( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( filterMatchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - + unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } +}; + +export default preFilter; diff --git a/src/selector/selectorError.js b/src/selector/selectorError.js new file mode 100644 index 000000000..a02e516da --- /dev/null +++ b/src/selector/selectorError.js @@ -0,0 +1,5 @@ +function selectorError( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +} + +export default selectorError; diff --git a/src/selector/testContext.js b/src/selector/testContext.js new file mode 100644 index 000000000..a54351e64 --- /dev/null +++ b/src/selector/testContext.js @@ -0,0 +1,10 @@ +/** + * Checks a node for validity as a jQuery selector context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +export default testContext; diff --git a/src/selector/toSelector.js b/src/selector/toSelector.js new file mode 100644 index 000000000..bd0c15f69 --- /dev/null +++ b/src/selector/toSelector.js @@ -0,0 +1,11 @@ +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +export default toSelector; diff --git a/src/selector/tokenize.js b/src/selector/tokenize.js new file mode 100644 index 000000000..34bc9ad50 --- /dev/null +++ b/src/selector/tokenize.js @@ -0,0 +1,83 @@ +import jQuery from "../core.js"; +import rcomma from "./var/rcomma.js"; +import rleadingCombinator from "./var/rleadingCombinator.js"; +import rtrim from "../var/rtrim.js"; +import createCache from "./createCache.js"; +import selectorError from "./selectorError.js"; +import filterMatchExpr from "./filterMatchExpr.js"; + +var tokenCache = createCache(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = jQuery.expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in filterMatchExpr ) { + if ( ( match = jQuery.expr.match[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + selectorError( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +export default tokenize; diff --git a/src/selector/unescapeSelector.js b/src/selector/unescapeSelector.js new file mode 100644 index 000000000..3a01f94bc --- /dev/null +++ b/src/selector/unescapeSelector.js @@ -0,0 +1,29 @@ +// CSS escapes +// https://www.w3.org/TR/CSS21/syndata.html#escaped-characters +import whitespace from "../var/whitespace.js"; + +var runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + // Strip the backslash prefix from a non-hex escape sequence + return nonHex; + } + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +function unescapeSelector( sel ) { + return sel.replace( runescape, funescape ); +} + +export default unescapeSelector; diff --git a/src/selector/var/attributes.js b/src/selector/var/attributes.js new file mode 100644 index 000000000..f9813acec --- /dev/null +++ b/src/selector/var/attributes.js @@ -0,0 +1,12 @@ +import whitespace from "../../var/whitespace.js"; +import identifier from "./identifier.js"; + +// Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors +export default "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]"; diff --git a/src/selector/var/booleans.js b/src/selector/var/booleans.js new file mode 100644 index 000000000..9dc3c97c7 --- /dev/null +++ b/src/selector/var/booleans.js @@ -0,0 +1,2 @@ +export default "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + + "loop|multiple|open|readonly|required|scoped"; diff --git a/src/selector/var/identifier.js b/src/selector/var/identifier.js new file mode 100644 index 000000000..03af0ddf3 --- /dev/null +++ b/src/selector/var/identifier.js @@ -0,0 +1,5 @@ +import whitespace from "../../var/whitespace.js"; + +// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram +export default "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+"; diff --git a/src/selector/var/matches.js b/src/selector/var/matches.js new file mode 100644 index 000000000..6f79452a1 --- /dev/null +++ b/src/selector/var/matches.js @@ -0,0 +1,5 @@ +import documentElement from "../../var/documentElement.js"; + +// Support: IE 9 - 11+ +// IE requires a prefix. +export default documentElement.matches || documentElement.msMatchesSelector; diff --git a/src/selector/var/pseudos.js b/src/selector/var/pseudos.js new file mode 100644 index 000000000..fdf288270 --- /dev/null +++ b/src/selector/var/pseudos.js @@ -0,0 +1,15 @@ +import identifier from "./identifier.js"; +import attributes from "./attributes.js"; + +export default ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)"; diff --git a/src/selector/var/rcomma.js b/src/selector/var/rcomma.js new file mode 100644 index 000000000..f5803f90f --- /dev/null +++ b/src/selector/var/rcomma.js @@ -0,0 +1,3 @@ +import whitespace from "../../var/whitespace.js"; + +export default new RegExp( "^" + whitespace + "*," + whitespace + "*" ); diff --git a/src/selector/var/rdescend.js b/src/selector/var/rdescend.js new file mode 100644 index 000000000..bab600901 --- /dev/null +++ b/src/selector/var/rdescend.js @@ -0,0 +1,3 @@ +import whitespace from "../../var/whitespace.js"; + +export default new RegExp( whitespace + "|>" ); diff --git a/src/selector/var/rleadingCombinator.js b/src/selector/var/rleadingCombinator.js new file mode 100644 index 000000000..f802dc8c0 --- /dev/null +++ b/src/selector/var/rleadingCombinator.js @@ -0,0 +1,4 @@ +import whitespace from "../../var/whitespace.js"; + +export default new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + + whitespace + "*" ); diff --git a/src/selector/var/rpseudo.js b/src/selector/var/rpseudo.js new file mode 100644 index 000000000..9ed943a5f --- /dev/null +++ b/src/selector/var/rpseudo.js @@ -0,0 +1,3 @@ +import pseudos from "./pseudos.js"; + +export default new RegExp( pseudos ); diff --git a/src/selector/var/rsibling.js b/src/selector/var/rsibling.js new file mode 100644 index 000000000..9aa815895 --- /dev/null +++ b/src/selector/var/rsibling.js @@ -0,0 +1 @@ +export default /[+~]/; diff --git a/test/unit/core.js b/test/unit/core.js index 49b7aa607..0029ee717 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -153,7 +153,7 @@ QUnit.test( "jQuery()", function( assert ) { "Empty attributes object is not interpreted as a document (trac-8950)" ); } ); -QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "jQuery(selector, context)", function( assert ) { +QUnit.test( "jQuery(selector, context)", function( assert ) { assert.expect( 3 ); assert.deepEqual( jQuery( "div p", "#qunit-fixture" ).get(), q( "sndp", "en", "sap" ), "Basic selector with string as context" ); assert.deepEqual( jQuery( "div p", q( "qunit-fixture" )[ 0 ] ).get(), q( "sndp", "en", "sap" ), "Basic selector with element as context" ); diff --git a/test/unit/selector.js b/test/unit/selector.js index 649b1e225..e368e827c 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -121,19 +121,13 @@ QUnit.test( "element", function( assert ) { assert.deepEqual( jQuery( "input[id='idTest']", lengthtest ).get(), q( "idTest" ), "Finding elements with id of ID." ); - if ( QUnit.jQuerySelectors ) { - siblingTest = document.getElementById( "siblingTest" ); - assert.deepEqual( jQuery( "div em", siblingTest ).get(), [], - "Element-rooted QSA does not select based on document context" ); - assert.deepEqual( jQuery( "div em, div em, div em:not(div em)", siblingTest ).get(), [], - "Element-rooted QSA does not select based on document context" ); - assert.deepEqual( jQuery( "div em, em\\,", siblingTest ).get(), [], - "Escaped commas do not get treated with an id in element-rooted QSA" ); - } else { - assert.ok( "skip", "Element-rooted QSA behavior different in selector-native" ); - assert.ok( "skip", "Element-rooted QSA behavior different in selector-native" ); - assert.ok( "skip", "Element-rooted QSA behavior different in selector-native" ); - } + siblingTest = document.getElementById( "siblingTest" ); + assert.deepEqual( jQuery( "div em", siblingTest ).get(), [], + "Element-rooted QSA does not select based on document context" ); + assert.deepEqual( jQuery( "div em, div em, div em:not(div em)", siblingTest ).get(), [], + "Element-rooted QSA does not select based on document context" ); + assert.deepEqual( jQuery( "div em, em\\,", siblingTest ).get(), [], + "Escaped commas do not get treated with an id in element-rooted QSA" ); html = ""; for ( i = 0; i < 100; i++ ) { @@ -263,12 +257,8 @@ QUnit.test( "id", function( assert ) { fiddle = jQuery( "