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.
		
		
		
		
		
			
		
			
				
					
					
						
							453 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
	
	
							453 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 | 
						|
};
 | 
						|
 |