1 | "use strict";
|
2 |
|
3 | module.exports = parse;
|
4 |
|
5 | var re_ws = /^\s/,
|
6 | re_name = /^(?:\\.|[\w\-\u00c0-\uFFFF])+/,
|
7 | re_escape = /\\([\da-f]{1,6}\s?|(\s)|.)/ig,
|
8 |
|
9 | re_attr = /^\s*((?:\\.|[\w\u00c0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])(.*?)\3|(#?(?:\\.|[\w\u00c0-\uFFFF\-])*)|)|)\s*(i)?\]/;
|
10 |
|
11 | var actionTypes = {
|
12 | __proto__: null,
|
13 | "undefined": "exists",
|
14 | "": "equals",
|
15 | "~": "element",
|
16 | "^": "start",
|
17 | "$": "end",
|
18 | "*": "any",
|
19 | "!": "not",
|
20 | "|": "hyphen"
|
21 | };
|
22 |
|
23 | var simpleSelectors = {
|
24 | __proto__: null,
|
25 | ">": "child",
|
26 | "<": "parent",
|
27 | "~": "sibling",
|
28 | "+": "adjacent"
|
29 | };
|
30 |
|
31 | var attribSelectors = {
|
32 | __proto__: null,
|
33 | "#": ["id", "equals"],
|
34 | ".": ["class", "element"]
|
35 | };
|
36 |
|
37 |
|
38 | function funescape( _, escaped, escapedWhitespace ) {
|
39 | var high = "0x" + escaped - 0x10000;
|
40 |
|
41 |
|
42 |
|
43 | return high !== high || escapedWhitespace ?
|
44 | escaped :
|
45 |
|
46 | high < 0 ?
|
47 | String.fromCharCode( high + 0x10000 ) :
|
48 |
|
49 | String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
|
50 | }
|
51 |
|
52 | function unescapeCSS(str){
|
53 | return str.replace(re_escape, funescape);
|
54 | }
|
55 |
|
56 | function getClosingPos(selector){
|
57 | var pos = 1, counter = 1, len = selector.length;
|
58 |
|
59 | for(; counter > 0 && pos < len; pos++){
|
60 | if(selector.charAt(pos) === "(") counter++;
|
61 | else if(selector.charAt(pos) === ")") counter--;
|
62 | }
|
63 |
|
64 | return pos;
|
65 | }
|
66 |
|
67 | function parse(selector, options){
|
68 | selector = (selector + "").trimLeft();
|
69 |
|
70 | var subselects = [],
|
71 | tokens = [],
|
72 | sawWS = false,
|
73 | data, firstChar, name;
|
74 |
|
75 | function getName(){
|
76 | var sub = selector.match(re_name)[0];
|
77 | selector = selector.substr(sub.length);
|
78 | return unescapeCSS(sub);
|
79 | }
|
80 |
|
81 | while(selector !== ""){
|
82 | if(re_name.test(selector)){
|
83 | if(sawWS){
|
84 | tokens.push({type: "descendant"});
|
85 | sawWS = false;
|
86 | }
|
87 |
|
88 | name = getName();
|
89 |
|
90 | if(!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)){
|
91 | name = name.toLowerCase();
|
92 | }
|
93 |
|
94 | tokens.push({type: "tag", name: name});
|
95 | } else if(re_ws.test(selector)){
|
96 | sawWS = true;
|
97 | selector = selector.trimLeft();
|
98 | } else {
|
99 | firstChar = selector.charAt(0);
|
100 | selector = selector.substr(1);
|
101 |
|
102 | if(firstChar in simpleSelectors){
|
103 | tokens.push({type: simpleSelectors[firstChar]});
|
104 | selector = selector.trimLeft();
|
105 | sawWS = false;
|
106 | continue;
|
107 | } else if(firstChar === ","){
|
108 | if(tokens.length === 0){
|
109 | throw new SyntaxError("empty sub-selector");
|
110 | }
|
111 | subselects.push(tokens);
|
112 | tokens = [];
|
113 |
|
114 | selector = selector.trimLeft();
|
115 | sawWS = false;
|
116 | continue;
|
117 | } else if(sawWS){
|
118 | tokens.push({type: "descendant"});
|
119 | sawWS = false;
|
120 | }
|
121 |
|
122 | if(firstChar === "*"){
|
123 | tokens.push({type: "universal"});
|
124 | } else if(firstChar in attribSelectors){
|
125 | tokens.push({
|
126 | type: "attribute",
|
127 | name: attribSelectors[firstChar][0],
|
128 | action: attribSelectors[firstChar][1],
|
129 | value: getName(),
|
130 | ignoreCase: false
|
131 | });
|
132 | } else if(firstChar === "["){
|
133 | data = selector.match(re_attr);
|
134 | if(!data){
|
135 | throw new SyntaxError("Malformed attribute selector: " + selector);
|
136 | }
|
137 | selector = selector.substr(data[0].length);
|
138 | name = unescapeCSS(data[1]);
|
139 |
|
140 | if(
|
141 | !options || (
|
142 | "lowerCaseAttributeNames" in options ?
|
143 | options.lowerCaseAttributeNames :
|
144 | !options.xmlMode
|
145 | )
|
146 | ){
|
147 | name = name.toLowerCase();
|
148 | }
|
149 |
|
150 | tokens.push({
|
151 | type: "attribute",
|
152 | name: name,
|
153 | action: actionTypes[data[2]],
|
154 | value: unescapeCSS(data[4] || data[5] || ""),
|
155 | ignoreCase: !!data[6]
|
156 | });
|
157 |
|
158 | } else if(firstChar === ":"){
|
159 |
|
160 | name = getName().toLowerCase();
|
161 | data = null;
|
162 |
|
163 | if(selector.charAt(0) === "("){
|
164 | var pos = getClosingPos(selector);
|
165 | data = selector.substr(1, pos - 2);
|
166 | selector = selector.substr(pos);
|
167 | }
|
168 |
|
169 | tokens.push({type: "pseudo", name: name, data: data});
|
170 | } else {
|
171 |
|
172 | throw new SyntaxError("Unmatched selector: " + firstChar + selector);
|
173 | }
|
174 | }
|
175 | }
|
176 |
|
177 | if(subselects.length > 0 && tokens.length === 0){
|
178 | throw new SyntaxError("empty sub-selector");
|
179 | }
|
180 | subselects.push(tokens);
|
181 | return subselects;
|
182 | } |
\ | No newline at end of file |