You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

finder.js 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. /*
  2. Slick Finder
  3. */"use strict"
  4. // Notable changes from Slick.Finder 1.0.x
  5. // faster bottom -> up expression matching
  6. // prefers mental sanity over *obsessive compulsive* milliseconds savings
  7. // uses prototypes instead of objects
  8. // tries to use matchesSelector smartly, whenever available
  9. // can populate objects as well as arrays
  10. // lots of stuff is broken or not implemented
  11. var parse = require("./parser")
  12. // utilities
  13. var index = 0,
  14. counter = document.__counter = (parseInt(document.__counter || -1, 36) + 1).toString(36),
  15. key = "uid:" + counter
  16. var uniqueID = function(n, xml){
  17. if (n === window) return "window"
  18. if (n === document) return "document"
  19. if (n === document.documentElement) return "html"
  20. if (xml) {
  21. var uid = n.getAttribute(key)
  22. if (!uid) {
  23. uid = (index++).toString(36)
  24. n.setAttribute(key, uid)
  25. }
  26. return uid
  27. } else {
  28. return n[key] || (n[key] = (index++).toString(36))
  29. }
  30. }
  31. var uniqueIDXML = function(n) {
  32. return uniqueID(n, true)
  33. }
  34. var isArray = Array.isArray || function(object){
  35. return Object.prototype.toString.call(object) === "[object Array]"
  36. }
  37. // tests
  38. var uniqueIndex = 0;
  39. var HAS = {
  40. GET_ELEMENT_BY_ID: function(test, id){
  41. id = "slick_" + (uniqueIndex++);
  42. // checks if the document has getElementById, and it works
  43. test.innerHTML = '<a id="' + id + '"></a>'
  44. return !!this.getElementById(id)
  45. },
  46. QUERY_SELECTOR: function(test){
  47. // this supposedly fixes a webkit bug with matchesSelector / querySelector & nth-child
  48. test.innerHTML = '_<style>:nth-child(2){}</style>'
  49. // checks if the document has querySelectorAll, and it works
  50. test.innerHTML = '<a class="MiX"></a>'
  51. return test.querySelectorAll('.MiX').length === 1
  52. },
  53. EXPANDOS: function(test, id){
  54. id = "slick_" + (uniqueIndex++);
  55. // checks if the document has elements that support expandos
  56. test._custom_property_ = id
  57. return test._custom_property_ === id
  58. },
  59. // TODO: use this ?
  60. // CHECKED_QUERY_SELECTOR: function(test){
  61. //
  62. // // checks if the document supports the checked query selector
  63. // test.innerHTML = '<select><option selected="selected">a</option></select>'
  64. // return test.querySelectorAll(':checked').length === 1
  65. // },
  66. // TODO: use this ?
  67. // EMPTY_ATTRIBUTE_QUERY_SELECTOR: function(test){
  68. //
  69. // // checks if the document supports the empty attribute query selector
  70. // test.innerHTML = '<a class=""></a>'
  71. // return test.querySelectorAll('[class*=""]').length === 1
  72. // },
  73. MATCHES_SELECTOR: function(test){
  74. test.className = "MiX"
  75. // checks if the document has matchesSelector, and we can use it.
  76. var matches = test.matchesSelector || test.mozMatchesSelector || test.webkitMatchesSelector
  77. // if matchesSelector trows errors on incorrect syntax we can use it
  78. if (matches) try {
  79. matches.call(test, ':slick')
  80. } catch(e){
  81. // just as a safety precaution, also test if it works on mixedcase (like querySelectorAll)
  82. return matches.call(test, ".MiX") ? matches : false
  83. }
  84. return false
  85. },
  86. GET_ELEMENTS_BY_CLASS_NAME: function(test){
  87. test.innerHTML = '<a class="f"></a><a class="b"></a>'
  88. if (test.getElementsByClassName('b').length !== 1) return false
  89. test.firstChild.className = 'b'
  90. if (test.getElementsByClassName('b').length !== 2) return false
  91. // Opera 9.6 getElementsByClassName doesnt detects the class if its not the first one
  92. test.innerHTML = '<a class="a"></a><a class="f b a"></a>'
  93. if (test.getElementsByClassName('a').length !== 2) return false
  94. // tests passed
  95. return true
  96. },
  97. // no need to know
  98. // GET_ELEMENT_BY_ID_NOT_NAME: function(test, id){
  99. // test.innerHTML = '<a name="'+ id +'"></a><b id="'+ id +'"></b>'
  100. // return this.getElementById(id) !== test.firstChild
  101. // },
  102. // this is always checked for and fixed
  103. // STAR_GET_ELEMENTS_BY_TAG_NAME: function(test){
  104. //
  105. // // IE returns comment nodes for getElementsByTagName('*') for some documents
  106. // test.appendChild(this.createComment(''))
  107. // if (test.getElementsByTagName('*').length > 0) return false
  108. //
  109. // // IE returns closed nodes (EG:"</foo>") for getElementsByTagName('*') for some documents
  110. // test.innerHTML = 'foo</foo>'
  111. // if (test.getElementsByTagName('*').length) return false
  112. //
  113. // // tests passed
  114. // return true
  115. // },
  116. // this is always checked for and fixed
  117. // STAR_QUERY_SELECTOR: function(test){
  118. //
  119. // // returns closed nodes (EG:"</foo>") for querySelector('*') for some documents
  120. // test.innerHTML = 'foo</foo>'
  121. // return !!(test.querySelectorAll('*').length)
  122. // },
  123. GET_ATTRIBUTE: function(test){
  124. // tests for working getAttribute implementation
  125. var shout = "fus ro dah"
  126. test.innerHTML = '<a class="' + shout + '"></a>'
  127. return test.firstChild.getAttribute('class') === shout
  128. }
  129. }
  130. // Finder
  131. var Finder = function Finder(document){
  132. this.document = document
  133. var root = this.root = document.documentElement
  134. this.tested = {}
  135. // uniqueID
  136. this.uniqueID = this.has("EXPANDOS") ? uniqueID : uniqueIDXML
  137. // getAttribute
  138. this.getAttribute = (this.has("GET_ATTRIBUTE")) ? function(node, name){
  139. return node.getAttribute(name)
  140. } : function(node, name){
  141. node = node.getAttributeNode(name)
  142. return (node && node.specified) ? node.value : null
  143. }
  144. // hasAttribute
  145. this.hasAttribute = (root.hasAttribute) ? function(node, attribute){
  146. return node.hasAttribute(attribute)
  147. } : function(node, attribute) {
  148. node = node.getAttributeNode(attribute)
  149. return !!(node && node.specified)
  150. }
  151. // contains
  152. this.contains = (document.contains && root.contains) ? function(context, node){
  153. return context.contains(node)
  154. } : (root.compareDocumentPosition) ? function(context, node){
  155. return context === node || !!(context.compareDocumentPosition(node) & 16)
  156. } : function(context, node){
  157. do {
  158. if (node === context) return true
  159. } while ((node = node.parentNode))
  160. return false
  161. }
  162. // sort
  163. // credits to Sizzle (http://sizzlejs.com/)
  164. this.sorter = (root.compareDocumentPosition) ? function(a, b){
  165. if (!a.compareDocumentPosition || !b.compareDocumentPosition) return 0
  166. return a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1
  167. } : ('sourceIndex' in root) ? function(a, b){
  168. if (!a.sourceIndex || !b.sourceIndex) return 0
  169. return a.sourceIndex - b.sourceIndex
  170. } : (document.createRange) ? function(a, b){
  171. if (!a.ownerDocument || !b.ownerDocument) return 0
  172. var aRange = a.ownerDocument.createRange(),
  173. bRange = b.ownerDocument.createRange()
  174. aRange.setStart(a, 0)
  175. aRange.setEnd(a, 0)
  176. bRange.setStart(b, 0)
  177. bRange.setEnd(b, 0)
  178. return aRange.compareBoundaryPoints(Range.START_TO_END, bRange)
  179. } : null
  180. this.failed = {}
  181. var nativeMatches = this.has("MATCHES_SELECTOR")
  182. if (nativeMatches) this.matchesSelector = function(node, expression){
  183. if (this.failed[expression]) return null
  184. try {
  185. return nativeMatches.call(node, expression)
  186. } catch(e){
  187. if (slick.debug) console.warn("matchesSelector failed on " + expression)
  188. this.failed[expression] = true
  189. return null
  190. }
  191. }
  192. if (this.has("QUERY_SELECTOR")){
  193. this.querySelectorAll = function(node, expression){
  194. if (this.failed[expression]) return true
  195. var result, _id, _expression, _combinator, _node
  196. // non-document rooted QSA
  197. // credits to Andrew Dupont
  198. if (node !== this.document){
  199. _combinator = expression[0].combinator
  200. _id = node.getAttribute("id")
  201. _expression = expression
  202. if (!_id){
  203. _node = node
  204. _id = "__slick__"
  205. _node.setAttribute("id", _id)
  206. }
  207. expression = "#" + _id + " " + _expression
  208. // these combinators need a parentNode due to how querySelectorAll works, which is:
  209. // finding all the elements that match the given selector
  210. // then filtering by the ones that have the specified element as an ancestor
  211. if (_combinator.indexOf("~") > -1 || _combinator.indexOf("+") > -1){
  212. node = node.parentNode
  213. if (!node) result = true
  214. // if node has no parentNode, we return "true" as if it failed, without polluting the failed cache
  215. }
  216. }
  217. if (!result) try {
  218. result = node.querySelectorAll(expression.toString())
  219. } catch(e){
  220. if (slick.debug) console.warn("querySelectorAll failed on " + (_expression || expression))
  221. result = this.failed[_expression || expression] = true
  222. }
  223. if (_node) _node.removeAttribute("id")
  224. return result
  225. }
  226. }
  227. }
  228. Finder.prototype.has = function(FEATURE){
  229. var tested = this.tested,
  230. testedFEATURE = tested[FEATURE]
  231. if (testedFEATURE != null) return testedFEATURE
  232. var root = this.root,
  233. document = this.document,
  234. testNode = document.createElement("div")
  235. testNode.setAttribute("style", "display: none;")
  236. root.appendChild(testNode)
  237. var TEST = HAS[FEATURE], result = false
  238. if (TEST) try {
  239. result = TEST.call(document, testNode)
  240. } catch(e){}
  241. if (slick.debug && !result) console.warn("document has no " + FEATURE)
  242. root.removeChild(testNode)
  243. return tested[FEATURE] = result
  244. }
  245. var combinators = {
  246. " ": function(node, part, push){
  247. var item, items
  248. var noId = !part.id, noTag = !part.tag, noClass = !part.classes
  249. if (part.id && node.getElementById && this.has("GET_ELEMENT_BY_ID")){
  250. item = node.getElementById(part.id)
  251. // return only if id is found, else keep checking
  252. // might be a tad slower on non-existing ids, but less insane
  253. if (item && item.getAttribute('id') === part.id){
  254. items = [item]
  255. noId = true
  256. // if tag is star, no need to check it in match()
  257. if (part.tag === "*") noTag = true
  258. }
  259. }
  260. if (!items){
  261. if (part.classes && node.getElementsByClassName && this.has("GET_ELEMENTS_BY_CLASS_NAME")){
  262. items = node.getElementsByClassName(part.classList)
  263. noClass = true
  264. // if tag is star, no need to check it in match()
  265. if (part.tag === "*") noTag = true
  266. } else {
  267. items = node.getElementsByTagName(part.tag)
  268. // if tag is star, need to check it in match because it could select junk, boho
  269. if (part.tag !== "*") noTag = true
  270. }
  271. if (!items || !items.length) return false
  272. }
  273. for (var i = 0; item = items[i++];)
  274. if ((noTag && noId && noClass && !part.attributes && !part.pseudos) || this.match(item, part, noTag, noId, noClass))
  275. push(item)
  276. return true
  277. },
  278. ">": function(node, part, push){ // direct children
  279. if ((node = node.firstChild)) do {
  280. if (node.nodeType == 1 && this.match(node, part)) push(node)
  281. } while ((node = node.nextSibling))
  282. },
  283. "+": function(node, part, push){ // next sibling
  284. while ((node = node.nextSibling)) if (node.nodeType == 1){
  285. if (this.match(node, part)) push(node)
  286. break
  287. }
  288. },
  289. "^": function(node, part, push){ // first child
  290. node = node.firstChild
  291. if (node){
  292. if (node.nodeType === 1){
  293. if (this.match(node, part)) push(node)
  294. } else {
  295. combinators['+'].call(this, node, part, push)
  296. }
  297. }
  298. },
  299. "~": function(node, part, push){ // next siblings
  300. while ((node = node.nextSibling)){
  301. if (node.nodeType === 1 && this.match(node, part)) push(node)
  302. }
  303. },
  304. "++": function(node, part, push){ // next sibling and previous sibling
  305. combinators['+'].call(this, node, part, push)
  306. combinators['!+'].call(this, node, part, push)
  307. },
  308. "~~": function(node, part, push){ // next siblings and previous siblings
  309. combinators['~'].call(this, node, part, push)
  310. combinators['!~'].call(this, node, part, push)
  311. },
  312. "!": function(node, part, push){ // all parent nodes up to document
  313. while ((node = node.parentNode)) if (node !== this.document && this.match(node, part)) push(node)
  314. },
  315. "!>": function(node, part, push){ // direct parent (one level)
  316. node = node.parentNode
  317. if (node !== this.document && this.match(node, part)) push(node)
  318. },
  319. "!+": function(node, part, push){ // previous sibling
  320. while ((node = node.previousSibling)) if (node.nodeType == 1){
  321. if (this.match(node, part)) push(node)
  322. break
  323. }
  324. },
  325. "!^": function(node, part, push){ // last child
  326. node = node.lastChild
  327. if (node){
  328. if (node.nodeType == 1){
  329. if (this.match(node, part)) push(node)
  330. } else {
  331. combinators['!+'].call(this, node, part, push)
  332. }
  333. }
  334. },
  335. "!~": function(node, part, push){ // previous siblings
  336. while ((node = node.previousSibling)){
  337. if (node.nodeType === 1 && this.match(node, part)) push(node)
  338. }
  339. }
  340. }
  341. Finder.prototype.search = function(context, expression, found){
  342. if (!context) context = this.document
  343. else if (!context.nodeType && context.document) context = context.document
  344. var expressions = parse(expression)
  345. // no expressions were parsed. todo: is this really necessary?
  346. if (!expressions || !expressions.length) throw new Error("invalid expression")
  347. if (!found) found = []
  348. var uniques, push = isArray(found) ? function(node){
  349. found[found.length] = node
  350. } : function(node){
  351. found[found.length++] = node
  352. }
  353. // if there is more than one expression we need to check for duplicates when we push to found
  354. // this simply saves the old push and wraps it around an uid dupe check.
  355. if (expressions.length > 1){
  356. uniques = {}
  357. var plush = push
  358. push = function(node){
  359. var uid = uniqueID(node)
  360. if (!uniques[uid]){
  361. uniques[uid] = true
  362. plush(node)
  363. }
  364. }
  365. }
  366. // walker
  367. var node, nodes, part
  368. main: for (var i = 0; expression = expressions[i++];){
  369. // querySelector
  370. // TODO: more functional tests
  371. // if there is querySelectorAll (and the expression does not fail) use it.
  372. if (!slick.noQSA && this.querySelectorAll){
  373. nodes = this.querySelectorAll(context, expression)
  374. if (nodes !== true){
  375. if (nodes && nodes.length) for (var j = 0; node = nodes[j++];) if (node.nodeName > '@'){
  376. push(node)
  377. }
  378. continue main
  379. }
  380. }
  381. // if there is only one part in the expression we don't need to check each part for duplicates.
  382. // todo: this might be too naive. while solid, there can be expression sequences that do not
  383. // produce duplicates. "body div" for instance, can never give you each div more than once.
  384. // "body div a" on the other hand might.
  385. if (expression.length === 1){
  386. part = expression[0]
  387. combinators[part.combinator].call(this, context, part, push)
  388. } else {
  389. var cs = [context], c, f, u, p = function(node){
  390. var uid = uniqueID(node)
  391. if (!u[uid]){
  392. u[uid] = true
  393. f[f.length] = node
  394. }
  395. }
  396. // loop the expression parts
  397. for (var j = 0; part = expression[j++];){
  398. f = []; u = {}
  399. // loop the contexts
  400. for (var k = 0; c = cs[k++];) combinators[part.combinator].call(this, c, part, p)
  401. // nothing was found, the expression failed, continue to the next expression.
  402. if (!f.length) continue main
  403. cs = f // set the contexts for future parts (if any)
  404. }
  405. if (i === 0) found = f // first expression. directly set found.
  406. else for (var l = 0; l < f.length; l++) push(f[l]) // any other expression needs to push to found.
  407. }
  408. }
  409. if (uniques && found && found.length > 1) this.sort(found)
  410. return found
  411. }
  412. Finder.prototype.sort = function(nodes){
  413. return this.sorter ? Array.prototype.sort.call(nodes, this.sorter) : nodes
  414. }
  415. // TODO: most of these pseudo selectors include <html> and qsa doesnt. fixme.
  416. var pseudos = {
  417. // TODO: returns different results than qsa empty.
  418. 'empty': function(){
  419. return !(this && this.nodeType === 1) && !(this.innerText || this.textContent || '').length
  420. },
  421. 'not': function(expression){
  422. return !slick.matches(this, expression)
  423. },
  424. 'contains': function(text){
  425. return (this.innerText || this.textContent || '').indexOf(text) > -1
  426. },
  427. 'first-child': function(){
  428. var node = this
  429. while ((node = node.previousSibling)) if (node.nodeType == 1) return false
  430. return true
  431. },
  432. 'last-child': function(){
  433. var node = this
  434. while ((node = node.nextSibling)) if (node.nodeType == 1) return false
  435. return true
  436. },
  437. 'only-child': function(){
  438. var prev = this
  439. while ((prev = prev.previousSibling)) if (prev.nodeType == 1) return false
  440. var next = this
  441. while ((next = next.nextSibling)) if (next.nodeType == 1) return false
  442. return true
  443. },
  444. 'first-of-type': function(){
  445. var node = this, nodeName = node.nodeName
  446. while ((node = node.previousSibling)) if (node.nodeName == nodeName) return false
  447. return true
  448. },
  449. 'last-of-type': function(){
  450. var node = this, nodeName = node.nodeName
  451. while ((node = node.nextSibling)) if (node.nodeName == nodeName) return false
  452. return true
  453. },
  454. 'only-of-type': function(){
  455. var prev = this, nodeName = this.nodeName
  456. while ((prev = prev.previousSibling)) if (prev.nodeName == nodeName) return false
  457. var next = this
  458. while ((next = next.nextSibling)) if (next.nodeName == nodeName) return false
  459. return true
  460. },
  461. 'enabled': function(){
  462. return !this.disabled
  463. },
  464. 'disabled': function(){
  465. return this.disabled
  466. },
  467. 'checked': function(){
  468. return this.checked || this.selected
  469. },
  470. 'selected': function(){
  471. return this.selected
  472. },
  473. 'focus': function(){
  474. var doc = this.ownerDocument
  475. return doc.activeElement === this && (this.href || this.type || slick.hasAttribute(this, 'tabindex'))
  476. },
  477. 'root': function(){
  478. return (this === this.ownerDocument.documentElement)
  479. }
  480. }
  481. Finder.prototype.match = function(node, bit, noTag, noId, noClass){
  482. // TODO: more functional tests ?
  483. if (!slick.noQSA && this.matchesSelector){
  484. var matches = this.matchesSelector(node, bit)
  485. if (matches !== null) return matches
  486. }
  487. // normal matching
  488. if (!noTag && bit.tag){
  489. var nodeName = node.nodeName.toLowerCase()
  490. if (bit.tag === "*"){
  491. if (nodeName < "@") return false
  492. } else if (nodeName != bit.tag){
  493. return false
  494. }
  495. }
  496. if (!noId && bit.id && node.getAttribute('id') !== bit.id) return false
  497. var i, part
  498. if (!noClass && bit.classes){
  499. var className = this.getAttribute(node, "class")
  500. if (!className) return false
  501. for (part in bit.classes) if (!RegExp('(^|\\s)' + bit.classes[part] + '(\\s|$)').test(className)) return false
  502. }
  503. var name, value
  504. if (bit.attributes) for (i = 0; part = bit.attributes[i++];){
  505. var operator = part.operator,
  506. escaped = part.escapedValue
  507. name = part.name
  508. value = part.value
  509. if (!operator){
  510. if (!this.hasAttribute(node, name)) return false
  511. } else {
  512. var actual = this.getAttribute(node, name)
  513. if (actual == null) return false
  514. switch (operator){
  515. case '^=' : if (!RegExp( '^' + escaped ).test(actual)) return false; break
  516. case '$=' : if (!RegExp( escaped + '$' ).test(actual)) return false; break
  517. case '~=' : if (!RegExp('(^|\\s)' + escaped + '(\\s|$)').test(actual)) return false; break
  518. case '|=' : if (!RegExp( '^' + escaped + '(-|$)' ).test(actual)) return false; break
  519. case '=' : if (actual !== value) return false; break
  520. case '*=' : if (actual.indexOf(value) === -1) return false; break
  521. default : return false
  522. }
  523. }
  524. }
  525. if (bit.pseudos) for (i = 0; part = bit.pseudos[i++];){
  526. name = part.name
  527. value = part.value
  528. if (pseudos[name]) return pseudos[name].call(node, value)
  529. if (value != null){
  530. if (this.getAttribute(node, name) !== value) return false
  531. } else {
  532. if (!this.hasAttribute(node, name)) return false
  533. }
  534. }
  535. return true
  536. }
  537. Finder.prototype.matches = function(node, expression){
  538. var expressions = parse(expression)
  539. if (expressions.length === 1 && expressions[0].length === 1){ // simplest match
  540. return this.match(node, expressions[0][0])
  541. }
  542. // TODO: more functional tests ?
  543. if (!slick.noQSA && this.matchesSelector){
  544. var matches = this.matchesSelector(node, expressions)
  545. if (matches !== null) return matches
  546. }
  547. var nodes = this.search(this.document, expression, {length: 0})
  548. for (var i = 0, res; res = nodes[i++];) if (node === res) return true
  549. return false
  550. }
  551. var finders = {}
  552. var finder = function(context){
  553. var doc = context || document
  554. if (doc.ownerDocument) doc = doc.ownerDocument
  555. else if (doc.document) doc = doc.document
  556. if (doc.nodeType !== 9) throw new TypeError("invalid document")
  557. var uid = uniqueID(doc)
  558. return finders[uid] || (finders[uid] = new Finder(doc))
  559. }
  560. // ... API ...
  561. var slick = function(expression, context){
  562. return slick.search(expression, context)
  563. }
  564. slick.search = function(expression, context, found){
  565. return finder(context).search(context, expression, found)
  566. }
  567. slick.find = function(expression, context){
  568. return finder(context).search(context, expression)[0] || null
  569. }
  570. slick.getAttribute = function(node, name){
  571. return finder(node).getAttribute(node, name)
  572. }
  573. slick.hasAttribute = function(node, name){
  574. return finder(node).hasAttribute(node, name)
  575. }
  576. slick.contains = function(context, node){
  577. return finder(context).contains(context, node)
  578. }
  579. slick.matches = function(node, expression){
  580. return finder(node).matches(node, expression)
  581. }
  582. slick.sort = function(nodes){
  583. if (nodes && nodes.length > 1) finder(nodes[0]).sort(nodes)
  584. return nodes
  585. }
  586. slick.parse = parse;
  587. // slick.debug = true
  588. // slick.noQSA = true
  589. module.exports = slick