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.

454 lines
14 KiB

/*
pseudo selectors
---
they are available in two forms:
* filters called when the selector
is compiled and return a function
that needs to return next()
* pseudos get called on execution
they need to return a boolean
*/
var getNCheck = require("nth-check");
var BaseFuncs = require("boolbase");
var attributes = require("./attributes.js");
var trueFunc = BaseFuncs.trueFunc;
var falseFunc = BaseFuncs.falseFunc;
var checkAttrib = attributes.rules.equals;
function getAttribFunc(name, value) {
var data = { name: name, value: value };
return function attribFunc(next, rule, options) {
return checkAttrib(next, data, options);
};
}
function getChildFunc(next, adapter) {
return function(elem) {
return !!adapter.getParent(elem) && next(elem);
};
}
var filters = {
contains: function(next, text, options) {
var adapter = options.adapter;
return function contains(elem) {
return next(elem) && adapter.getText(elem).indexOf(text) >= 0;
};
},
icontains: function(next, text, options) {
var itext = text.toLowerCase();
var adapter = options.adapter;
return function icontains(elem) {
return (
next(elem) &&
adapter
.getText(elem)
.toLowerCase()
.indexOf(itext) >= 0
);
};
},
//location specific methods
"nth-child": function(next, rule, options) {
var func = getNCheck(rule);
var adapter = options.adapter;
if (func === falseFunc) return func;
if (func === trueFunc) return getChildFunc(next, adapter);
return function nthChild(elem) {
var siblings = adapter.getSiblings(elem);
for (var i = 0, pos = 0; i < siblings.length; i++) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) break;
else pos++;
}
}
return func(pos) && next(elem);
};
},
"nth-last-child": function(next, rule, options) {
var func = getNCheck(rule);
var adapter = options.adapter;
if (func === falseFunc) return func;
if (func === trueFunc) return getChildFunc(next, adapter);
return function nthLastChild(elem) {
var siblings = adapter.getSiblings(elem);
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) break;
else pos++;
}
}
return func(pos) && next(elem);
};
},
"nth-of-type": function(next, rule, options) {
var func = getNCheck(rule);
var adapter = options.adapter;
if (func === falseFunc) return func;
if (func === trueFunc) return getChildFunc(next, adapter);
return function nthOfType(elem) {
var siblings = adapter.getSiblings(elem);
for (var pos = 0, i = 0; i < siblings.length; i++) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) break;
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
}
}
return func(pos) && next(elem);
};
},
"nth-last-of-type": function(next, rule, options) {
var func = getNCheck(rule);
var adapter = options.adapter;
if (func === falseFunc) return func;
if (func === trueFunc) return getChildFunc(next, adapter);
return function nthLastOfType(elem) {
var siblings = adapter.getSiblings(elem);
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) break;
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
}
}
return func(pos) && next(elem);
};
},
//TODO determine the actual root element
root: function(next, rule, options) {
var adapter = options.adapter;
return function(elem) {
return !adapter.getParent(elem) && next(elem);
};
},
scope: function(next, rule, options, context) {
var adapter = options.adapter;
if (!context || context.length === 0) {
//equivalent to :root
return filters.root(next, rule, options);
}
function equals(a, b) {
if (typeof adapter.equals === "function") return adapter.equals(a, b);
return a === b;
}
if (context.length === 1) {
//NOTE: can't be unpacked, as :has uses this for side-effects
return function(elem) {
return equals(context[0], elem) && next(elem);
};
}
return function(elem) {
return context.indexOf(elem) >= 0 && next(elem);
};
},
//jQuery extensions (others follow as pseudos)
checkbox: getAttribFunc("type", "checkbox"),
file: getAttribFunc("type", "file"),
password: getAttribFunc("type", "password"),
radio: getAttribFunc("type", "radio"),
reset: getAttribFunc("type", "reset"),
image: getAttribFunc("type", "image"),
submit: getAttribFunc("type", "submit"),
//dynamic state pseudos. These depend on optional Adapter methods.
hover: function(next, rule, options) {
var adapter = options.adapter;
if (typeof adapter.isHovered === 'function') {
return function hover(elem) {
return next(elem) && adapter.isHovered(elem);
};
}
return falseFunc;
},
visited: function(next, rule, options) {
var adapter = options.adapter;
if (typeof adapter.isVisited === 'function') {
return function visited(elem) {
return next(elem) && adapter.isVisited(elem);
};
}
return falseFunc;
},
active: function(next, rule, options) {
var adapter = options.adapter;
if (typeof adapter.isActive === 'function') {
return function active(elem) {
return next(elem) && adapter.isActive(elem);
};
}
return falseFunc;
}
};
//helper methods
function getFirstElement(elems, adapter) {
for (var i = 0; elems && i < elems.length; i++) {
if (adapter.isTag(elems[i])) return elems[i];
}
}
//while filters are precompiled, pseudos get called when they are needed
var pseudos = {
empty: function(elem, adapter) {
return !adapter.getChildren(elem).some(function(elem) {
return adapter.isTag(elem) || elem.type === "text";
});
},
"first-child": function(elem, adapter) {
return getFirstElement(adapter.getSiblings(elem), adapter) === elem;
},
"last-child": function(elem, adapter) {
var siblings = adapter.getSiblings(elem);
for (var i = siblings.length - 1; i >= 0; i--) {
if (siblings[i] === elem) return true;
if (adapter.isTag(siblings[i])) break;
}
return false;
},
"first-of-type": function(elem, adapter) {
var siblings = adapter.getSiblings(elem);
for (var i = 0; i < siblings.length; i++) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) return true;
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
}
}
return false;
},
"last-of-type": function(elem, adapter) {
var siblings = adapter.getSiblings(elem);
for (var i = siblings.length - 1; i >= 0; i--) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) return true;
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
}
}
return false;
},
"only-of-type": function(elem, adapter) {
var siblings = adapter.getSiblings(elem);
for (var i = 0, j = siblings.length; i < j; i++) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) continue;
if (adapter.getName(siblings[i]) === adapter.getName(elem)) {
return false;
}
}
}
return true;
},
"only-child": function(elem, adapter) {
var siblings = adapter.getSiblings(elem);
for (var i = 0; i < siblings.length; i++) {
if (adapter.isTag(siblings[i]) && siblings[i] !== elem) return false;
}
return true;
},
//:matches(a, area, link)[href]
link: function(elem, adapter) {
return adapter.hasAttrib(elem, "href");
},
//TODO: :any-link once the name is finalized (as an alias of :link)
//forms
//to consider: :target
//:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
selected: function(elem, adapter) {
if (adapter.hasAttrib(elem, "selected")) return true;
else if (adapter.getName(elem) !== "option") return false;
//the first <option> in a <select> is also selected
var parent = adapter.getParent(elem);
if (!parent || adapter.getName(parent) !== "select" || adapter.hasAttrib(parent, "multiple")) {
return false;
}
var siblings = adapter.getChildren(parent);
var sawElem = false;
for (var i = 0; i < siblings.length; i++) {
if (adapter.isTag(siblings[i])) {
if (siblings[i] === elem) {
sawElem = true;
} else if (!sawElem) {
return false;
} else if (adapter.hasAttrib(siblings[i], "selected")) {
return false;
}
}
}
return sawElem;
},
//https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
//:matches(
// :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
// optgroup[disabled] > option),
// fieldset[disabled] * //TODO not child of first <legend>
//)
disabled: function(elem, adapter) {
return adapter.hasAttrib(elem, "disabled");
},
enabled: function(elem, adapter) {
return !adapter.hasAttrib(elem, "disabled");
},
//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
checked: function(elem, adapter) {
return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem, adapter);
},
//:matches(input, select, textarea)[required]
required: function(elem, adapter) {
return adapter.hasAttrib(elem, "required");
},
//:matches(input, select, textarea):not([required])
optional: function(elem, adapter) {
return !adapter.hasAttrib(elem, "required");
},
//jQuery extensions
//:not(:empty)
parent: function(elem, adapter) {
return !pseudos.empty(elem, adapter);
},
//:matches(h1, h2, h3, h4, h5, h6)
header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
//:matches(button, input[type=button])
button: function(elem, adapter) {
var name = adapter.getName(elem);
return (
name === "button" || (name === "input" && adapter.getAttributeValue(elem, "type") === "button")
);
},
//:matches(input, textarea, select, button)
input: namePseudo(["input", "textarea", "select", "button"]),
//input:matches(:not([type!='']), [type='text' i])
text: function(elem, adapter) {
var attr;
return (
adapter.getName(elem) === "input" &&
(!(attr = adapter.getAttributeValue(elem, "type")) || attr.toLowerCase() === "text")
);
}
};
function namePseudo(names) {
if (typeof Set !== "undefined") {
// eslint-disable-next-line no-undef
var nameSet = new Set(names);
return function(elem, adapter) {
return nameSet.has(adapter.getName(elem));
};
}
return function(elem, adapter) {
return names.indexOf(adapter.getName(elem)) >= 0;
};
}
function verifyArgs(func, name, subselect) {
if (subselect === null) {
if (func.length > 2 && name !== "scope") {
throw new Error("pseudo-selector :" + name + " requires an argument");
}
} else {
if (func.length === 2) {
throw new Error("pseudo-selector :" + name + " doesn't have any arguments");
}
}
}
//FIXME this feels hacky
var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
module.exports = {
compile: function(next, data, options, context) {
var name = data.name;
var subselect = data.data;
var adapter = options.adapter;
if (options && options.strict && !re_CSS3.test(name)) {
throw new Error(":" + name + " isn't part of CSS3");
}
if (typeof filters[name] === "function") {
return filters[name](next, subselect, options, context);
} else if (typeof pseudos[name] === "function") {
var func = pseudos[name];
verifyArgs(func, name, subselect);
if (func === falseFunc) {
return func;
}
if (next === trueFunc) {
return function pseudoRoot(elem) {
return func(elem, adapter, subselect);
};
}
return function pseudoArgs(elem) {
return func(elem, adapter, subselect) && next(elem);
};
} else {
throw new Error("unmatched pseudo-class :" + name);
}
},
filters: filters,
pseudos: pseudos
};