275 lines
6.3 KiB
JavaScript
275 lines
6.3 KiB
JavaScript
"use strict";
|
|
|
|
module.exports = parse;
|
|
|
|
var re_name = /^(?:\\.|[\w\-\u00b0-\uFFFF])+/,
|
|
re_escape = /\\([\da-f]{1,6}\s?|(\s)|.)/ig,
|
|
//modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
|
|
re_attr = /^\s*((?:\\.|[\w\u00b0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])([^]*?)\3|(#?(?:\\.|[\w\u00b0-\uFFFF\-])*)|)|)\s*(i)?\]/;
|
|
|
|
var actionTypes = {
|
|
__proto__: null,
|
|
"undefined": "exists",
|
|
"": "equals",
|
|
"~": "element",
|
|
"^": "start",
|
|
"$": "end",
|
|
"*": "any",
|
|
"!": "not",
|
|
"|": "hyphen"
|
|
};
|
|
|
|
var simpleSelectors = {
|
|
__proto__: null,
|
|
">": "child",
|
|
"<": "parent",
|
|
"~": "sibling",
|
|
"+": "adjacent"
|
|
};
|
|
|
|
var attribSelectors = {
|
|
__proto__: null,
|
|
"#": ["id", "equals"],
|
|
".": ["class", "element"]
|
|
};
|
|
|
|
//pseudos, whose data-property is parsed as well
|
|
var unpackPseudos = {
|
|
__proto__: null,
|
|
"has": true,
|
|
"not": true,
|
|
"matches": true
|
|
};
|
|
|
|
var stripQuotesFromPseudos = {
|
|
__proto__: null,
|
|
"contains": true,
|
|
"icontains": true
|
|
};
|
|
|
|
var quotes = {
|
|
__proto__: null,
|
|
"\"": true,
|
|
"'": true
|
|
};
|
|
|
|
//unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L139
|
|
function funescape( _, escaped, escapedWhitespace ) {
|
|
var high = "0x" + escaped - 0x10000;
|
|
// NaN means non-codepoint
|
|
// Support: Firefox
|
|
// Workaround erroneous numeric interpretation of +"0x"
|
|
return high !== high || escapedWhitespace ?
|
|
escaped :
|
|
// BMP codepoint
|
|
high < 0 ?
|
|
String.fromCharCode( high + 0x10000 ) :
|
|
// Supplemental Plane codepoint (surrogate pair)
|
|
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
|
|
}
|
|
|
|
function unescapeCSS(str){
|
|
return str.replace(re_escape, funescape);
|
|
}
|
|
|
|
function isWhitespace(c){
|
|
return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r";
|
|
}
|
|
|
|
function parse(selector, options){
|
|
var subselects = [];
|
|
|
|
selector = parseSelector(subselects, selector + "", options);
|
|
|
|
if(selector !== ""){
|
|
throw new SyntaxError("Unmatched selector: " + selector);
|
|
}
|
|
|
|
return subselects;
|
|
}
|
|
|
|
function parseSelector(subselects, selector, options){
|
|
var tokens = [],
|
|
sawWS = false,
|
|
data, firstChar, name, quot;
|
|
|
|
function getName(){
|
|
var sub = selector.match(re_name)[0];
|
|
selector = selector.substr(sub.length);
|
|
return unescapeCSS(sub);
|
|
}
|
|
|
|
function stripWhitespace(start){
|
|
while(isWhitespace(selector.charAt(start))) start++;
|
|
selector = selector.substr(start);
|
|
}
|
|
|
|
function isEscaped(pos) {
|
|
var slashCount = 0;
|
|
|
|
while (selector.charAt(--pos) === "\\") slashCount++;
|
|
return (slashCount & 1) === 1;
|
|
}
|
|
|
|
stripWhitespace(0);
|
|
|
|
while(selector !== ""){
|
|
firstChar = selector.charAt(0);
|
|
|
|
if(isWhitespace(firstChar)){
|
|
sawWS = true;
|
|
stripWhitespace(1);
|
|
} else if(firstChar in simpleSelectors){
|
|
tokens.push({type: simpleSelectors[firstChar]});
|
|
sawWS = false;
|
|
|
|
stripWhitespace(1);
|
|
} else if(firstChar === ","){
|
|
if(tokens.length === 0){
|
|
throw new SyntaxError("empty sub-selector");
|
|
}
|
|
subselects.push(tokens);
|
|
tokens = [];
|
|
sawWS = false;
|
|
stripWhitespace(1);
|
|
} else {
|
|
if(sawWS){
|
|
if(tokens.length > 0){
|
|
tokens.push({type: "descendant"});
|
|
}
|
|
sawWS = false;
|
|
}
|
|
|
|
if(firstChar === "*"){
|
|
selector = selector.substr(1);
|
|
tokens.push({type: "universal"});
|
|
} else if(firstChar in attribSelectors){
|
|
selector = selector.substr(1);
|
|
tokens.push({
|
|
type: "attribute",
|
|
name: attribSelectors[firstChar][0],
|
|
action: attribSelectors[firstChar][1],
|
|
value: getName(),
|
|
ignoreCase: false
|
|
});
|
|
} else if(firstChar === "["){
|
|
selector = selector.substr(1);
|
|
data = selector.match(re_attr);
|
|
if(!data){
|
|
throw new SyntaxError("Malformed attribute selector: " + selector);
|
|
}
|
|
selector = selector.substr(data[0].length);
|
|
name = unescapeCSS(data[1]);
|
|
|
|
if(
|
|
!options || (
|
|
"lowerCaseAttributeNames" in options ?
|
|
options.lowerCaseAttributeNames :
|
|
!options.xmlMode
|
|
)
|
|
){
|
|
name = name.toLowerCase();
|
|
}
|
|
|
|
tokens.push({
|
|
type: "attribute",
|
|
name: name,
|
|
action: actionTypes[data[2]],
|
|
value: unescapeCSS(data[4] || data[5] || ""),
|
|
ignoreCase: !!data[6]
|
|
});
|
|
|
|
} else if(firstChar === ":"){
|
|
if(selector.charAt(1) === ":"){
|
|
selector = selector.substr(2);
|
|
tokens.push({type: "pseudo-element", name: getName().toLowerCase()});
|
|
continue;
|
|
}
|
|
|
|
selector = selector.substr(1);
|
|
|
|
name = getName().toLowerCase();
|
|
data = null;
|
|
|
|
if(selector.charAt(0) === "("){
|
|
if(name in unpackPseudos){
|
|
quot = selector.charAt(1);
|
|
var quoted = quot in quotes;
|
|
|
|
selector = selector.substr(quoted + 1);
|
|
|
|
data = [];
|
|
selector = parseSelector(data, selector, options);
|
|
|
|
if(quoted){
|
|
if(selector.charAt(0) !== quot){
|
|
throw new SyntaxError("unmatched quotes in :" + name);
|
|
} else {
|
|
selector = selector.substr(1);
|
|
}
|
|
}
|
|
|
|
if(selector.charAt(0) !== ")"){
|
|
throw new SyntaxError("missing closing parenthesis in :" + name + " " + selector);
|
|
}
|
|
|
|
selector = selector.substr(1);
|
|
} else {
|
|
var pos = 1, counter = 1;
|
|
|
|
for(; counter > 0 && pos < selector.length; pos++){
|
|
if(selector.charAt(pos) === "(" && !isEscaped(pos)) counter++;
|
|
else if(selector.charAt(pos) === ")" && !isEscaped(pos)) counter--;
|
|
}
|
|
|
|
if(counter){
|
|
throw new SyntaxError("parenthesis not matched");
|
|
}
|
|
|
|
data = selector.substr(1, pos - 2);
|
|
selector = selector.substr(pos);
|
|
|
|
if(name in stripQuotesFromPseudos){
|
|
quot = data.charAt(0);
|
|
|
|
if(quot === data.slice(-1) && quot in quotes){
|
|
data = data.slice(1, -1);
|
|
}
|
|
|
|
data = unescapeCSS(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
tokens.push({type: "pseudo", name: name, data: data});
|
|
} else if(re_name.test(selector)){
|
|
name = getName();
|
|
|
|
if(!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)){
|
|
name = name.toLowerCase();
|
|
}
|
|
|
|
tokens.push({type: "tag", name: name});
|
|
} else {
|
|
if(tokens.length && tokens[tokens.length - 1].type === "descendant"){
|
|
tokens.pop();
|
|
}
|
|
addToken(subselects, tokens);
|
|
return selector;
|
|
}
|
|
}
|
|
}
|
|
|
|
addToken(subselects, tokens);
|
|
|
|
return selector;
|
|
}
|
|
|
|
function addToken(subselects, tokens){
|
|
if(subselects.length > 0 && tokens.length === 0){
|
|
throw new SyntaxError("empty sub-selector");
|
|
}
|
|
|
|
subselects.push(tokens);
|
|
}
|