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.
394 lines
9.7 KiB
394 lines
9.7 KiB
4 years ago
|
/*
|
||
|
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 DomUtils = require("domutils"),
|
||
|
isTag = DomUtils.isTag,
|
||
|
getText = DomUtils.getText,
|
||
|
getParent = DomUtils.getParent,
|
||
|
getChildren = DomUtils.getChildren,
|
||
|
getSiblings = DomUtils.getSiblings,
|
||
|
hasAttrib = DomUtils.hasAttrib,
|
||
|
getName = DomUtils.getName,
|
||
|
getAttribute= DomUtils.getAttributeValue,
|
||
|
getNCheck = require("nth-check"),
|
||
|
checkAttrib = require("./attributes.js").rules.equals,
|
||
|
BaseFuncs = require("boolbase"),
|
||
|
trueFunc = BaseFuncs.trueFunc,
|
||
|
falseFunc = BaseFuncs.falseFunc;
|
||
|
|
||
|
//helper methods
|
||
|
function getFirstElement(elems){
|
||
|
for(var i = 0; elems && i < elems.length; i++){
|
||
|
if(isTag(elems[i])) return elems[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getAttribFunc(name, value){
|
||
|
var data = {name: name, value: value};
|
||
|
return function attribFunc(next){
|
||
|
return checkAttrib(next, data);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getChildFunc(next){
|
||
|
return function(elem){
|
||
|
return !!getParent(elem) && next(elem);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var filters = {
|
||
|
contains: function(next, text){
|
||
|
return function contains(elem){
|
||
|
return next(elem) && getText(elem).indexOf(text) >= 0;
|
||
|
};
|
||
|
},
|
||
|
icontains: function(next, text){
|
||
|
var itext = text.toLowerCase();
|
||
|
return function icontains(elem){
|
||
|
return next(elem) &&
|
||
|
getText(elem).toLowerCase().indexOf(itext) >= 0;
|
||
|
};
|
||
|
},
|
||
|
|
||
|
//location specific methods
|
||
|
"nth-child": function(next, rule){
|
||
|
var func = getNCheck(rule);
|
||
|
|
||
|
if(func === falseFunc) return func;
|
||
|
if(func === trueFunc) return getChildFunc(next);
|
||
|
|
||
|
return function nthChild(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = 0, pos = 0; i < siblings.length; i++){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) break;
|
||
|
else pos++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return func(pos) && next(elem);
|
||
|
};
|
||
|
},
|
||
|
"nth-last-child": function(next, rule){
|
||
|
var func = getNCheck(rule);
|
||
|
|
||
|
if(func === falseFunc) return func;
|
||
|
if(func === trueFunc) return getChildFunc(next);
|
||
|
|
||
|
return function nthLastChild(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) break;
|
||
|
else pos++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return func(pos) && next(elem);
|
||
|
};
|
||
|
},
|
||
|
"nth-of-type": function(next, rule){
|
||
|
var func = getNCheck(rule);
|
||
|
|
||
|
if(func === falseFunc) return func;
|
||
|
if(func === trueFunc) return getChildFunc(next);
|
||
|
|
||
|
return function nthOfType(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var pos = 0, i = 0; i < siblings.length; i++){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) break;
|
||
|
if(getName(siblings[i]) === getName(elem)) pos++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return func(pos) && next(elem);
|
||
|
};
|
||
|
},
|
||
|
"nth-last-of-type": function(next, rule){
|
||
|
var func = getNCheck(rule);
|
||
|
|
||
|
if(func === falseFunc) return func;
|
||
|
if(func === trueFunc) return getChildFunc(next);
|
||
|
|
||
|
return function nthLastOfType(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) break;
|
||
|
if(getName(siblings[i]) === getName(elem)) pos++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return func(pos) && next(elem);
|
||
|
};
|
||
|
},
|
||
|
|
||
|
//TODO determine the actual root element
|
||
|
root: function(next){
|
||
|
return function(elem){
|
||
|
return !getParent(elem) && next(elem);
|
||
|
};
|
||
|
},
|
||
|
|
||
|
scope: function(next, rule, options, context){
|
||
|
if(!context || context.length === 0){
|
||
|
//equivalent to :root
|
||
|
return filters.root(next);
|
||
|
}
|
||
|
|
||
|
if(context.length === 1){
|
||
|
//NOTE: can't be unpacked, as :has uses this for side-effects
|
||
|
return function(elem){
|
||
|
return 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")
|
||
|
};
|
||
|
|
||
|
//while filters are precompiled, pseudos get called when they are needed
|
||
|
var pseudos = {
|
||
|
empty: function(elem){
|
||
|
return !getChildren(elem).some(function(elem){
|
||
|
return isTag(elem) || elem.type === "text";
|
||
|
});
|
||
|
},
|
||
|
|
||
|
"first-child": function(elem){
|
||
|
return getFirstElement(getSiblings(elem)) === elem;
|
||
|
},
|
||
|
"last-child": function(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = siblings.length - 1; i >= 0; i--){
|
||
|
if(siblings[i] === elem) return true;
|
||
|
if(isTag(siblings[i])) break;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
"first-of-type": function(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = 0; i < siblings.length; i++){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) return true;
|
||
|
if(getName(siblings[i]) === getName(elem)) break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
"last-of-type": function(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = siblings.length-1; i >= 0; i--){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) return true;
|
||
|
if(getName(siblings[i]) === getName(elem)) break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
"only-of-type": function(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = 0, j = siblings.length; i < j; i++){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem) continue;
|
||
|
if(getName(siblings[i]) === getName(elem)) return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
"only-child": function(elem){
|
||
|
var siblings = getSiblings(elem);
|
||
|
|
||
|
for(var i = 0; i < siblings.length; i++){
|
||
|
if(isTag(siblings[i]) && siblings[i] !== elem) return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
//:matches(a, area, link)[href]
|
||
|
link: function(elem){
|
||
|
return hasAttrib(elem, "href");
|
||
|
},
|
||
|
visited: falseFunc, //seems to be a valid implementation
|
||
|
//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){
|
||
|
if(hasAttrib(elem, "selected")) return true;
|
||
|
else if(getName(elem) !== "option") return false;
|
||
|
|
||
|
//the first <option> in a <select> is also selected
|
||
|
var parent = getParent(elem);
|
||
|
|
||
|
if(
|
||
|
!parent ||
|
||
|
getName(parent) !== "select" ||
|
||
|
hasAttrib(parent, "multiple")
|
||
|
) return false;
|
||
|
|
||
|
var siblings = getChildren(parent),
|
||
|
sawElem = false;
|
||
|
|
||
|
for(var i = 0; i < siblings.length; i++){
|
||
|
if(isTag(siblings[i])){
|
||
|
if(siblings[i] === elem){
|
||
|
sawElem = true;
|
||
|
} else if(!sawElem){
|
||
|
return false;
|
||
|
} else if(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){
|
||
|
return hasAttrib(elem, "disabled");
|
||
|
},
|
||
|
enabled: function(elem){
|
||
|
return !hasAttrib(elem, "disabled");
|
||
|
},
|
||
|
//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
|
||
|
checked: function(elem){
|
||
|
return hasAttrib(elem, "checked") || pseudos.selected(elem);
|
||
|
},
|
||
|
//:matches(input, select, textarea)[required]
|
||
|
required: function(elem){
|
||
|
return hasAttrib(elem, "required");
|
||
|
},
|
||
|
//:matches(input, select, textarea):not([required])
|
||
|
optional: function(elem){
|
||
|
return !hasAttrib(elem, "required");
|
||
|
},
|
||
|
|
||
|
//jQuery extensions
|
||
|
|
||
|
//:not(:empty)
|
||
|
parent: function(elem){
|
||
|
return !pseudos.empty(elem);
|
||
|
},
|
||
|
//:matches(h1, h2, h3, h4, h5, h6)
|
||
|
header: function(elem){
|
||
|
var name = getName(elem);
|
||
|
return name === "h1" ||
|
||
|
name === "h2" ||
|
||
|
name === "h3" ||
|
||
|
name === "h4" ||
|
||
|
name === "h5" ||
|
||
|
name === "h6";
|
||
|
},
|
||
|
|
||
|
//:matches(button, input[type=button])
|
||
|
button: function(elem){
|
||
|
var name = getName(elem);
|
||
|
return name === "button" ||
|
||
|
name === "input" &&
|
||
|
getAttribute(elem, "type") === "button";
|
||
|
},
|
||
|
//:matches(input, textarea, select, button)
|
||
|
input: function(elem){
|
||
|
var name = getName(elem);
|
||
|
return name === "input" ||
|
||
|
name === "textarea" ||
|
||
|
name === "select" ||
|
||
|
name === "button";
|
||
|
},
|
||
|
//input:matches(:not([type!='']), [type='text' i])
|
||
|
text: function(elem){
|
||
|
var attr;
|
||
|
return getName(elem) === "input" && (
|
||
|
!(attr = getAttribute(elem, "type")) ||
|
||
|
attr.toLowerCase() === "text"
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function verifyArgs(func, name, subselect){
|
||
|
if(subselect === null){
|
||
|
if(func.length > 1 && name !== "scope"){
|
||
|
throw new SyntaxError("pseudo-selector :" + name + " requires an argument");
|
||
|
}
|
||
|
} else {
|
||
|
if(func.length === 1){
|
||
|
throw new SyntaxError("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,
|
||
|
subselect = data.data;
|
||
|
|
||
|
if(options && options.strict && !re_CSS3.test(name)){
|
||
|
throw SyntaxError(":" + name + " isn't part of CSS3");
|
||
|
}
|
||
|
|
||
|
if(typeof filters[name] === "function"){
|
||
|
verifyArgs(filters[name], name, subselect);
|
||
|
return filters[name](next, subselect, options, context);
|
||
|
} else if(typeof pseudos[name] === "function"){
|
||
|
var func = pseudos[name];
|
||
|
verifyArgs(func, name, subselect);
|
||
|
|
||
|
if(next === trueFunc) return func;
|
||
|
|
||
|
return function pseudoArgs(elem){
|
||
|
return func(elem, subselect) && next(elem);
|
||
|
};
|
||
|
} else {
|
||
|
throw new SyntaxError("unmatched pseudo-class :" + name);
|
||
|
}
|
||
|
},
|
||
|
filters: filters,
|
||
|
pseudos: pseudos
|
||
|
};
|