| /* |
| 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 |
| }; |