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.

453 lines
14 KiB

4 years ago
  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. //dynamic state pseudos. These depend on optional Adapter methods.
  149. hover: function(next, rule, options) {
  150. var adapter = options.adapter;
  151. if (typeof adapter.isHovered === 'function') {
  152. return function hover(elem) {
  153. return next(elem) && adapter.isHovered(elem);
  154. };
  155. }
  156. return falseFunc;
  157. },
  158. visited: function(next, rule, options) {
  159. var adapter = options.adapter;
  160. if (typeof adapter.isVisited === 'function') {
  161. return function visited(elem) {
  162. return next(elem) && adapter.isVisited(elem);
  163. };
  164. }
  165. return falseFunc;
  166. },
  167. active: function(next, rule, options) {
  168. var adapter = options.adapter;
  169. if (typeof adapter.isActive === 'function') {
  170. return function active(elem) {
  171. return next(elem) && adapter.isActive(elem);
  172. };
  173. }
  174. return falseFunc;
  175. }
  176. };
  177. //helper methods
  178. function getFirstElement(elems, adapter) {
  179. for (var i = 0; elems && i < elems.length; i++) {
  180. if (adapter.isTag(elems[i])) return elems[i];
  181. }
  182. }
  183. //while filters are precompiled, pseudos get called when they are needed
  184. var pseudos = {
  185. empty: function(elem, adapter) {
  186. return !adapter.getChildren(elem).some(function(elem) {
  187. return adapter.isTag(elem) || elem.type === "text";
  188. });
  189. },
  190. "first-child": function(elem, adapter) {
  191. return getFirstElement(adapter.getSiblings(elem), adapter) === elem;
  192. },
  193. "last-child": function(elem, adapter) {
  194. var siblings = adapter.getSiblings(elem);
  195. for (var i = siblings.length - 1; i >= 0; i--) {
  196. if (siblings[i] === elem) return true;
  197. if (adapter.isTag(siblings[i])) break;
  198. }
  199. return false;
  200. },
  201. "first-of-type": function(elem, adapter) {
  202. var siblings = adapter.getSiblings(elem);
  203. for (var i = 0; i < siblings.length; i++) {
  204. if (adapter.isTag(siblings[i])) {
  205. if (siblings[i] === elem) return true;
  206. if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
  207. }
  208. }
  209. return false;
  210. },
  211. "last-of-type": function(elem, adapter) {
  212. var siblings = adapter.getSiblings(elem);
  213. for (var i = siblings.length - 1; i >= 0; i--) {
  214. if (adapter.isTag(siblings[i])) {
  215. if (siblings[i] === elem) return true;
  216. if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
  217. }
  218. }
  219. return false;
  220. },
  221. "only-of-type": function(elem, adapter) {
  222. var siblings = adapter.getSiblings(elem);
  223. for (var i = 0, j = siblings.length; i < j; i++) {
  224. if (adapter.isTag(siblings[i])) {
  225. if (siblings[i] === elem) continue;
  226. if (adapter.getName(siblings[i]) === adapter.getName(elem)) {
  227. return false;
  228. }
  229. }
  230. }
  231. return true;
  232. },
  233. "only-child": function(elem, adapter) {
  234. var siblings = adapter.getSiblings(elem);
  235. for (var i = 0; i < siblings.length; i++) {
  236. if (adapter.isTag(siblings[i]) && siblings[i] !== elem) return false;
  237. }
  238. return true;
  239. },
  240. //:matches(a, area, link)[href]
  241. link: function(elem, adapter) {
  242. return adapter.hasAttrib(elem, "href");
  243. },
  244. //TODO: :any-link once the name is finalized (as an alias of :link)
  245. //forms
  246. //to consider: :target
  247. //:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
  248. selected: function(elem, adapter) {
  249. if (adapter.hasAttrib(elem, "selected")) return true;
  250. else if (adapter.getName(elem) !== "option") return false;
  251. //the first <option> in a <select> is also selected
  252. var parent = adapter.getParent(elem);
  253. if (!parent || adapter.getName(parent) !== "select" || adapter.hasAttrib(parent, "multiple")) {
  254. return false;
  255. }
  256. var siblings = adapter.getChildren(parent);
  257. var sawElem = false;
  258. for (var i = 0; i < siblings.length; i++) {
  259. if (adapter.isTag(siblings[i])) {
  260. if (siblings[i] === elem) {
  261. sawElem = true;
  262. } else if (!sawElem) {
  263. return false;
  264. } else if (adapter.hasAttrib(siblings[i], "selected")) {
  265. return false;
  266. }
  267. }
  268. }
  269. return sawElem;
  270. },
  271. //https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
  272. //:matches(
  273. // :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
  274. // optgroup[disabled] > option),
  275. // fieldset[disabled] * //TODO not child of first <legend>
  276. //)
  277. disabled: function(elem, adapter) {
  278. return adapter.hasAttrib(elem, "disabled");
  279. },
  280. enabled: function(elem, adapter) {
  281. return !adapter.hasAttrib(elem, "disabled");
  282. },
  283. //:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
  284. checked: function(elem, adapter) {
  285. return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem, adapter);
  286. },
  287. //:matches(input, select, textarea)[required]
  288. required: function(elem, adapter) {
  289. return adapter.hasAttrib(elem, "required");
  290. },
  291. //:matches(input, select, textarea):not([required])
  292. optional: function(elem, adapter) {
  293. return !adapter.hasAttrib(elem, "required");
  294. },
  295. //jQuery extensions
  296. //:not(:empty)
  297. parent: function(elem, adapter) {
  298. return !pseudos.empty(elem, adapter);
  299. },
  300. //:matches(h1, h2, h3, h4, h5, h6)
  301. header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
  302. //:matches(button, input[type=button])
  303. button: function(elem, adapter) {
  304. var name = adapter.getName(elem);
  305. return (
  306. name === "button" || (name === "input" && adapter.getAttributeValue(elem, "type") === "button")
  307. );
  308. },
  309. //:matches(input, textarea, select, button)
  310. input: namePseudo(["input", "textarea", "select", "button"]),
  311. //input:matches(:not([type!='']), [type='text' i])
  312. text: function(elem, adapter) {
  313. var attr;
  314. return (
  315. adapter.getName(elem) === "input" &&
  316. (!(attr = adapter.getAttributeValue(elem, "type")) || attr.toLowerCase() === "text")
  317. );
  318. }
  319. };
  320. function namePseudo(names) {
  321. if (typeof Set !== "undefined") {
  322. // eslint-disable-next-line no-undef
  323. var nameSet = new Set(names);
  324. return function(elem, adapter) {
  325. return nameSet.has(adapter.getName(elem));
  326. };
  327. }
  328. return function(elem, adapter) {
  329. return names.indexOf(adapter.getName(elem)) >= 0;
  330. };
  331. }
  332. function verifyArgs(func, name, subselect) {
  333. if (subselect === null) {
  334. if (func.length > 2 && name !== "scope") {
  335. throw new Error("pseudo-selector :" + name + " requires an argument");
  336. }
  337. } else {
  338. if (func.length === 2) {
  339. throw new Error("pseudo-selector :" + name + " doesn't have any arguments");
  340. }
  341. }
  342. }
  343. //FIXME this feels hacky
  344. var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
  345. module.exports = {
  346. compile: function(next, data, options, context) {
  347. var name = data.name;
  348. var subselect = data.data;
  349. var adapter = options.adapter;
  350. if (options && options.strict && !re_CSS3.test(name)) {
  351. throw new Error(":" + name + " isn't part of CSS3");
  352. }
  353. if (typeof filters[name] === "function") {
  354. return filters[name](next, subselect, options, context);
  355. } else if (typeof pseudos[name] === "function") {
  356. var func = pseudos[name];
  357. verifyArgs(func, name, subselect);
  358. if (func === falseFunc) {
  359. return func;
  360. }
  361. if (next === trueFunc) {
  362. return function pseudoRoot(elem) {
  363. return func(elem, adapter, subselect);
  364. };
  365. }
  366. return function pseudoArgs(elem) {
  367. return func(elem, adapter, subselect) && next(elem);
  368. };
  369. } else {
  370. throw new Error("unmatched pseudo-class :" + name);
  371. }
  372. },
  373. filters: filters,
  374. pseudos: pseudos
  375. };