pseudos.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /*
  2. pseudo selectors
  3. ---
  4. they are available in two forms:
  5. * filters called when the selector
  6. is compiled and return a function
  7. that needs to return next()
  8. * pseudos get called on execution
  9. they need to return a boolean
  10. */
  11. var getNCheck = require("nth-check");
  12. var BaseFuncs = require("boolbase");
  13. var attributes = require("./attributes.js");
  14. var trueFunc = BaseFuncs.trueFunc;
  15. var falseFunc = BaseFuncs.falseFunc;
  16. var checkAttrib = attributes.rules.equals;
  17. function getAttribFunc(name, value) {
  18. var data = { name: name, value: value };
  19. return function attribFunc(next, rule, options) {
  20. return checkAttrib(next, data, options);
  21. };
  22. }
  23. function getChildFunc(next, adapter) {
  24. return function(elem) {
  25. return !!adapter.getParent(elem) && next(elem);
  26. };
  27. }
  28. var filters = {
  29. contains: function(next, text, options) {
  30. var adapter = options.adapter;
  31. return function contains(elem) {
  32. return next(elem) && adapter.getText(elem).indexOf(text) >= 0;
  33. };
  34. },
  35. icontains: function(next, text, options) {
  36. var itext = text.toLowerCase();
  37. var adapter = options.adapter;
  38. return function icontains(elem) {
  39. return (
  40. next(elem) &&
  41. adapter
  42. .getText(elem)
  43. .toLowerCase()
  44. .indexOf(itext) >= 0
  45. );
  46. };
  47. },
  48. //location specific methods
  49. "nth-child": function(next, rule, options) {
  50. var func = getNCheck(rule);
  51. var adapter = options.adapter;
  52. if (func === falseFunc) return func;
  53. if (func === trueFunc) return getChildFunc(next, adapter);
  54. return function nthChild(elem) {
  55. var siblings = adapter.getSiblings(elem);
  56. for (var i = 0, pos = 0; i < siblings.length; i++) {
  57. if (adapter.isTag(siblings[i])) {
  58. if (siblings[i] === elem) break;
  59. else pos++;
  60. }
  61. }
  62. return func(pos) && next(elem);
  63. };
  64. },
  65. "nth-last-child": function(next, rule, options) {
  66. var func = getNCheck(rule);
  67. var adapter = options.adapter;
  68. if (func === falseFunc) return func;
  69. if (func === trueFunc) return getChildFunc(next, adapter);
  70. return function nthLastChild(elem) {
  71. var siblings = adapter.getSiblings(elem);
  72. for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
  73. if (adapter.isTag(siblings[i])) {
  74. if (siblings[i] === elem) break;
  75. else pos++;
  76. }
  77. }
  78. return func(pos) && next(elem);
  79. };
  80. },
  81. "nth-of-type": function(next, rule, options) {
  82. var func = getNCheck(rule);
  83. var adapter = options.adapter;
  84. if (func === falseFunc) return func;
  85. if (func === trueFunc) return getChildFunc(next, adapter);
  86. return function nthOfType(elem) {
  87. var siblings = adapter.getSiblings(elem);
  88. for (var pos = 0, i = 0; i < siblings.length; i++) {
  89. if (adapter.isTag(siblings[i])) {
  90. if (siblings[i] === elem) break;
  91. if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
  92. }
  93. }
  94. return func(pos) && next(elem);
  95. };
  96. },
  97. "nth-last-of-type": function(next, rule, options) {
  98. var func = getNCheck(rule);
  99. var adapter = options.adapter;
  100. if (func === falseFunc) return func;
  101. if (func === trueFunc) return getChildFunc(next, adapter);
  102. return function nthLastOfType(elem) {
  103. var siblings = adapter.getSiblings(elem);
  104. for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
  105. if (adapter.isTag(siblings[i])) {
  106. if (siblings[i] === elem) break;
  107. if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
  108. }
  109. }
  110. return func(pos) && next(elem);
  111. };
  112. },
  113. //TODO determine the actual root element
  114. root: function(next, rule, options) {
  115. var adapter = options.adapter;
  116. return function(elem) {
  117. return !adapter.getParent(elem) && next(elem);
  118. };
  119. },
  120. scope: function(next, rule, options, context) {
  121. var adapter = options.adapter;
  122. if (!context || context.length === 0) {
  123. //equivalent to :root
  124. return filters.root(next, rule, options);
  125. }
  126. function equals(a, b) {
  127. if (typeof adapter.equals === "function") return adapter.equals(a, b);
  128. return a === b;
  129. }
  130. if (context.length === 1) {
  131. //NOTE: can't be unpacked, as :has uses this for side-effects
  132. return function(elem) {
  133. return equals(context[0], elem) && next(elem);
  134. };
  135. }
  136. return function(elem) {
  137. return context.indexOf(elem) >= 0 && next(elem);
  138. };
  139. },
  140. //jQuery extensions (others follow as pseudos)
  141. checkbox: getAttribFunc("type", "checkbox"),
  142. file: getAttribFunc("type", "file"),
  143. password: getAttribFunc("type", "password"),
  144. radio: getAttribFunc("type", "radio"),
  145. reset: getAttribFunc("type", "reset"),
  146. image: getAttribFunc("type", "image"),
  147. submit: getAttribFunc("type", "submit")
  148. };
  149. //helper methods
  150. function getFirstElement(elems, adapter) {
  151. for (var i = 0; elems && i < elems.length; i++) {
  152. if (adapter.isTag(elems[i])) return elems[i];
  153. }
  154. }
  155. //while filters are precompiled, pseudos get called when they are needed
  156. var pseudos = {
  157. empty: function(elem, adapter) {
  158. return !adapter.getChildren(elem).some(function(elem) {
  159. return adapter.isTag(elem) || elem.type === "text";
  160. });
  161. },
  162. "first-child": function(elem, adapter) {
  163. return getFirstElement(adapter.getSiblings(elem), adapter) === elem;
  164. },
  165. "last-child": function(elem, adapter) {
  166. var siblings = adapter.getSiblings(elem);
  167. for (var i = siblings.length - 1; i >= 0; i--) {
  168. if (siblings[i] === elem) return true;
  169. if (adapter.isTag(siblings[i])) break;
  170. }
  171. return false;
  172. },
  173. "first-of-type": function(elem, adapter) {
  174. var siblings = adapter.getSiblings(elem);
  175. for (var i = 0; i < siblings.length; i++) {
  176. if (adapter.isTag(siblings[i])) {
  177. if (siblings[i] === elem) return true;
  178. if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
  179. }
  180. }
  181. return false;
  182. },
  183. "last-of-type": function(elem, adapter) {
  184. var siblings = adapter.getSiblings(elem);
  185. for (var i = siblings.length - 1; i >= 0; i--) {
  186. if (adapter.isTag(siblings[i])) {
  187. if (siblings[i] === elem) return true;
  188. if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
  189. }
  190. }
  191. return false;
  192. },
  193. "only-of-type": function(elem, adapter) {
  194. var siblings = adapter.getSiblings(elem);
  195. for (var i = 0, j = siblings.length; i < j; i++) {
  196. if (adapter.isTag(siblings[i])) {
  197. if (siblings[i] === elem) continue;
  198. if (adapter.getName(siblings[i]) === adapter.getName(elem)) {
  199. return false;
  200. }
  201. }
  202. }
  203. return true;
  204. },
  205. "only-child": function(elem, adapter) {
  206. var siblings = adapter.getSiblings(elem);
  207. for (var i = 0; i < siblings.length; i++) {
  208. if (adapter.isTag(siblings[i]) && siblings[i] !== elem) return false;
  209. }
  210. return true;
  211. },
  212. //:matches(a, area, link)[href]
  213. link: function(elem, adapter) {
  214. return adapter.hasAttrib(elem, "href");
  215. },
  216. visited: falseFunc, //Valid implementation
  217. //TODO: :any-link once the name is finalized (as an alias of :link)
  218. //forms
  219. //to consider: :target
  220. //:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
  221. selected: function(elem, adapter) {
  222. if (adapter.hasAttrib(elem, "selected")) return true;
  223. else if (adapter.getName(elem) !== "option") return false;
  224. //the first <option> in a <select> is also selected
  225. var parent = adapter.getParent(elem);
  226. if (!parent || adapter.getName(parent) !== "select" || adapter.hasAttrib(parent, "multiple")) {
  227. return false;
  228. }
  229. var siblings = adapter.getChildren(parent);
  230. var sawElem = false;
  231. for (var i = 0; i < siblings.length; i++) {
  232. if (adapter.isTag(siblings[i])) {
  233. if (siblings[i] === elem) {
  234. sawElem = true;
  235. } else if (!sawElem) {
  236. return false;
  237. } else if (adapter.hasAttrib(siblings[i], "selected")) {
  238. return false;
  239. }
  240. }
  241. }
  242. return sawElem;
  243. },
  244. //https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
  245. //:matches(
  246. // :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
  247. // optgroup[disabled] > option),
  248. // fieldset[disabled] * //TODO not child of first <legend>
  249. //)
  250. disabled: function(elem, adapter) {
  251. return adapter.hasAttrib(elem, "disabled");
  252. },
  253. enabled: function(elem, adapter) {
  254. return !adapter.hasAttrib(elem, "disabled");
  255. },
  256. //:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
  257. checked: function(elem, adapter) {
  258. return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem, adapter);
  259. },
  260. //:matches(input, select, textarea)[required]
  261. required: function(elem, adapter) {
  262. return adapter.hasAttrib(elem, "required");
  263. },
  264. //:matches(input, select, textarea):not([required])
  265. optional: function(elem, adapter) {
  266. return !adapter.hasAttrib(elem, "required");
  267. },
  268. //jQuery extensions
  269. //:not(:empty)
  270. parent: function(elem, adapter) {
  271. return !pseudos.empty(elem, adapter);
  272. },
  273. //:matches(h1, h2, h3, h4, h5, h6)
  274. header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
  275. //:matches(button, input[type=button])
  276. button: function(elem, adapter) {
  277. var name = adapter.getName(elem);
  278. return (
  279. name === "button" || (name === "input" && adapter.getAttributeValue(elem, "type") === "button")
  280. );
  281. },
  282. //:matches(input, textarea, select, button)
  283. input: namePseudo(["input", "textarea", "select", "button"]),
  284. //input:matches(:not([type!='']), [type='text' i])
  285. text: function(elem, adapter) {
  286. var attr;
  287. return (
  288. adapter.getName(elem) === "input" &&
  289. (!(attr = adapter.getAttributeValue(elem, "type")) || attr.toLowerCase() === "text")
  290. );
  291. }
  292. };
  293. function namePseudo(names) {
  294. if (typeof Set !== "undefined") {
  295. // eslint-disable-next-line no-undef
  296. var nameSet = new Set(names);
  297. return function(elem, adapter) {
  298. return nameSet.has(adapter.getName(elem));
  299. };
  300. }
  301. return function(elem, adapter) {
  302. return names.indexOf(adapter.getName(elem)) >= 0;
  303. };
  304. }
  305. function verifyArgs(func, name, subselect) {
  306. if (subselect === null) {
  307. if (func.length > 2 && name !== "scope") {
  308. throw new Error("pseudo-selector :" + name + " requires an argument");
  309. }
  310. } else {
  311. if (func.length === 2) {
  312. throw new Error("pseudo-selector :" + name + " doesn't have any arguments");
  313. }
  314. }
  315. }
  316. //FIXME this feels hacky
  317. var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
  318. module.exports = {
  319. compile: function(next, data, options, context) {
  320. var name = data.name;
  321. var subselect = data.data;
  322. var adapter = options.adapter;
  323. if (options && options.strict && !re_CSS3.test(name)) {
  324. throw new Error(":" + name + " isn't part of CSS3");
  325. }
  326. if (typeof filters[name] === "function") {
  327. return filters[name](next, subselect, options, context);
  328. } else if (typeof pseudos[name] === "function") {
  329. var func = pseudos[name];
  330. verifyArgs(func, name, subselect);
  331. if (func === falseFunc) {
  332. return func;
  333. }
  334. if (next === trueFunc) {
  335. return function pseudoRoot(elem) {
  336. return func(elem, adapter, subselect);
  337. };
  338. }
  339. return function pseudoArgs(elem) {
  340. return func(elem, adapter, subselect) && next(elem);
  341. };
  342. } else {
  343. throw new Error("unmatched pseudo-class :" + name);
  344. }
  345. },
  346. filters: filters,
  347. pseudos: pseudos
  348. };