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.
267 lines
6.1 KiB
267 lines
6.1 KiB
"use strict"; |
|
|
|
module.exports = parse; |
|
|
|
var re_name = /^(?:\\.|[\w\-\u00c0-\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\u00c0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])(.*?)\3|(#?(?:\\.|[\w\u00c0-\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); |
|
} |
|
|
|
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) === "(") counter++; |
|
else if(selector.charAt(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); |
|
}
|
|
|