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