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.

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