index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. "use strict";
  2. module.exports = parse;
  3. var re_name = /^(?:\\.|[\w\-\u00b0-\uFFFF])+/,
  4. re_escape = /\\([\da-f]{1,6}\s?|(\s)|.)/ig,
  5. //modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
  6. re_attr = /^\s*((?:\\.|[\w\u00b0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])([^]*?)\3|(#?(?:\\.|[\w\u00b0-\uFFFF\-])*)|)|)\s*(i)?\]/;
  7. var actionTypes = {
  8. __proto__: null,
  9. "undefined": "exists",
  10. "": "equals",
  11. "~": "element",
  12. "^": "start",
  13. "$": "end",
  14. "*": "any",
  15. "!": "not",
  16. "|": "hyphen"
  17. };
  18. var simpleSelectors = {
  19. __proto__: null,
  20. ">": "child",
  21. "<": "parent",
  22. "~": "sibling",
  23. "+": "adjacent"
  24. };
  25. var attribSelectors = {
  26. __proto__: null,
  27. "#": ["id", "equals"],
  28. ".": ["class", "element"]
  29. };
  30. //pseudos, whose data-property is parsed as well
  31. var unpackPseudos = {
  32. __proto__: null,
  33. "has": true,
  34. "not": true,
  35. "matches": true
  36. };
  37. var stripQuotesFromPseudos = {
  38. __proto__: null,
  39. "contains": true,
  40. "icontains": true
  41. };
  42. var quotes = {
  43. __proto__: null,
  44. "\"": true,
  45. "'": true
  46. };
  47. //unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L139
  48. function funescape( _, escaped, escapedWhitespace ) {
  49. var high = "0x" + escaped - 0x10000;
  50. // NaN means non-codepoint
  51. // Support: Firefox
  52. // Workaround erroneous numeric interpretation of +"0x"
  53. return high !== high || escapedWhitespace ?
  54. escaped :
  55. // BMP codepoint
  56. high < 0 ?
  57. String.fromCharCode( high + 0x10000 ) :
  58. // Supplemental Plane codepoint (surrogate pair)
  59. String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
  60. }
  61. function unescapeCSS(str){
  62. return str.replace(re_escape, funescape);
  63. }
  64. function isWhitespace(c){
  65. return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r";
  66. }
  67. function parse(selector, options){
  68. var subselects = [];
  69. selector = parseSelector(subselects, selector + "", options);
  70. if(selector !== ""){
  71. throw new SyntaxError("Unmatched selector: " + selector);
  72. }
  73. return subselects;
  74. }
  75. function parseSelector(subselects, selector, options){
  76. var tokens = [],
  77. sawWS = false,
  78. data, firstChar, name, quot;
  79. function getName(){
  80. var sub = selector.match(re_name)[0];
  81. selector = selector.substr(sub.length);
  82. return unescapeCSS(sub);
  83. }
  84. function stripWhitespace(start){
  85. while(isWhitespace(selector.charAt(start))) start++;
  86. selector = selector.substr(start);
  87. }
  88. function isEscaped(pos) {
  89. var slashCount = 0;
  90. while (selector.charAt(--pos) === "\\") slashCount++;
  91. return (slashCount & 1) === 1;
  92. }
  93. stripWhitespace(0);
  94. while(selector !== ""){
  95. firstChar = selector.charAt(0);
  96. if(isWhitespace(firstChar)){
  97. sawWS = true;
  98. stripWhitespace(1);
  99. } else if(firstChar in simpleSelectors){
  100. tokens.push({type: simpleSelectors[firstChar]});
  101. sawWS = false;
  102. stripWhitespace(1);
  103. } else if(firstChar === ","){
  104. if(tokens.length === 0){
  105. throw new SyntaxError("empty sub-selector");
  106. }
  107. subselects.push(tokens);
  108. tokens = [];
  109. sawWS = false;
  110. stripWhitespace(1);
  111. } else {
  112. if(sawWS){
  113. if(tokens.length > 0){
  114. tokens.push({type: "descendant"});
  115. }
  116. sawWS = false;
  117. }
  118. if(firstChar === "*"){
  119. selector = selector.substr(1);
  120. tokens.push({type: "universal"});
  121. } else if(firstChar in attribSelectors){
  122. selector = selector.substr(1);
  123. tokens.push({
  124. type: "attribute",
  125. name: attribSelectors[firstChar][0],
  126. action: attribSelectors[firstChar][1],
  127. value: getName(),
  128. ignoreCase: false
  129. });
  130. } else if(firstChar === "["){
  131. selector = selector.substr(1);
  132. data = selector.match(re_attr);
  133. if(!data){
  134. throw new SyntaxError("Malformed attribute selector: " + selector);
  135. }
  136. selector = selector.substr(data[0].length);
  137. name = unescapeCSS(data[1]);
  138. if(
  139. !options || (
  140. "lowerCaseAttributeNames" in options ?
  141. options.lowerCaseAttributeNames :
  142. !options.xmlMode
  143. )
  144. ){
  145. name = name.toLowerCase();
  146. }
  147. tokens.push({
  148. type: "attribute",
  149. name: name,
  150. action: actionTypes[data[2]],
  151. value: unescapeCSS(data[4] || data[5] || ""),
  152. ignoreCase: !!data[6]
  153. });
  154. } else if(firstChar === ":"){
  155. if(selector.charAt(1) === ":"){
  156. selector = selector.substr(2);
  157. tokens.push({type: "pseudo-element", name: getName().toLowerCase()});
  158. continue;
  159. }
  160. selector = selector.substr(1);
  161. name = getName().toLowerCase();
  162. data = null;
  163. if(selector.charAt(0) === "("){
  164. if(name in unpackPseudos){
  165. quot = selector.charAt(1);
  166. var quoted = quot in quotes;
  167. selector = selector.substr(quoted + 1);
  168. data = [];
  169. selector = parseSelector(data, selector, options);
  170. if(quoted){
  171. if(selector.charAt(0) !== quot){
  172. throw new SyntaxError("unmatched quotes in :" + name);
  173. } else {
  174. selector = selector.substr(1);
  175. }
  176. }
  177. if(selector.charAt(0) !== ")"){
  178. throw new SyntaxError("missing closing parenthesis in :" + name + " " + selector);
  179. }
  180. selector = selector.substr(1);
  181. } else {
  182. var pos = 1, counter = 1;
  183. for(; counter > 0 && pos < selector.length; pos++){
  184. if(selector.charAt(pos) === "(" && !isEscaped(pos)) counter++;
  185. else if(selector.charAt(pos) === ")" && !isEscaped(pos)) counter--;
  186. }
  187. if(counter){
  188. throw new SyntaxError("parenthesis not matched");
  189. }
  190. data = selector.substr(1, pos - 2);
  191. selector = selector.substr(pos);
  192. if(name in stripQuotesFromPseudos){
  193. quot = data.charAt(0);
  194. if(quot === data.slice(-1) && quot in quotes){
  195. data = data.slice(1, -1);
  196. }
  197. data = unescapeCSS(data);
  198. }
  199. }
  200. }
  201. tokens.push({type: "pseudo", name: name, data: data});
  202. } else if(re_name.test(selector)){
  203. name = getName();
  204. if(!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)){
  205. name = name.toLowerCase();
  206. }
  207. tokens.push({type: "tag", name: name});
  208. } else {
  209. if(tokens.length && tokens[tokens.length - 1].type === "descendant"){
  210. tokens.pop();
  211. }
  212. addToken(subselects, tokens);
  213. return selector;
  214. }
  215. }
  216. }
  217. addToken(subselects, tokens);
  218. return selector;
  219. }
  220. function addToken(subselects, tokens){
  221. if(subselects.length > 0 && tokens.length === 0){
  222. throw new SyntaxError("empty sub-selector");
  223. }
  224. subselects.push(tokens);
  225. }