index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. "use strict";
  2. module.exports = parse;
  3. var re_name = /^(?:\\.|[\w\-\u00c0-\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\u00c0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])(.*?)\3|(#?(?:\\.|[\w\u00c0-\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. stripWhitespace(0);
  89. while(selector !== ""){
  90. firstChar = selector.charAt(0);
  91. if(isWhitespace(firstChar)){
  92. sawWS = true;
  93. stripWhitespace(1);
  94. } else if(firstChar in simpleSelectors){
  95. tokens.push({type: simpleSelectors[firstChar]});
  96. sawWS = false;
  97. stripWhitespace(1);
  98. } else if(firstChar === ","){
  99. if(tokens.length === 0){
  100. throw new SyntaxError("empty sub-selector");
  101. }
  102. subselects.push(tokens);
  103. tokens = [];
  104. sawWS = false;
  105. stripWhitespace(1);
  106. } else {
  107. if(sawWS){
  108. if(tokens.length > 0){
  109. tokens.push({type: "descendant"});
  110. }
  111. sawWS = false;
  112. }
  113. if(firstChar === "*"){
  114. selector = selector.substr(1);
  115. tokens.push({type: "universal"});
  116. } else if(firstChar in attribSelectors){
  117. selector = selector.substr(1);
  118. tokens.push({
  119. type: "attribute",
  120. name: attribSelectors[firstChar][0],
  121. action: attribSelectors[firstChar][1],
  122. value: getName(),
  123. ignoreCase: false
  124. });
  125. } else if(firstChar === "["){
  126. selector = selector.substr(1);
  127. data = selector.match(re_attr);
  128. if(!data){
  129. throw new SyntaxError("Malformed attribute selector: " + selector);
  130. }
  131. selector = selector.substr(data[0].length);
  132. name = unescapeCSS(data[1]);
  133. if(
  134. !options || (
  135. "lowerCaseAttributeNames" in options ?
  136. options.lowerCaseAttributeNames :
  137. !options.xmlMode
  138. )
  139. ){
  140. name = name.toLowerCase();
  141. }
  142. tokens.push({
  143. type: "attribute",
  144. name: name,
  145. action: actionTypes[data[2]],
  146. value: unescapeCSS(data[4] || data[5] || ""),
  147. ignoreCase: !!data[6]
  148. });
  149. } else if(firstChar === ":"){
  150. if(selector.charAt(1) === ":"){
  151. selector = selector.substr(2);
  152. tokens.push({type: "pseudo-element", name: getName().toLowerCase()});
  153. continue;
  154. }
  155. selector = selector.substr(1);
  156. name = getName().toLowerCase();
  157. data = null;
  158. if(selector.charAt(0) === "("){
  159. if(name in unpackPseudos){
  160. quot = selector.charAt(1);
  161. var quoted = quot in quotes;
  162. selector = selector.substr(quoted + 1);
  163. data = [];
  164. selector = parseSelector(data, selector, options);
  165. if(quoted){
  166. if(selector.charAt(0) !== quot){
  167. throw new SyntaxError("unmatched quotes in :" + name);
  168. } else {
  169. selector = selector.substr(1);
  170. }
  171. }
  172. if(selector.charAt(0) !== ")"){
  173. throw new SyntaxError("missing closing parenthesis in :" + name + " " + selector);
  174. }
  175. selector = selector.substr(1);
  176. } else {
  177. var pos = 1, counter = 1;
  178. for(; counter > 0 && pos < selector.length; pos++){
  179. if(selector.charAt(pos) === "(") counter++;
  180. else if(selector.charAt(pos) === ")") counter--;
  181. }
  182. if(counter){
  183. throw new SyntaxError("parenthesis not matched");
  184. }
  185. data = selector.substr(1, pos - 2);
  186. selector = selector.substr(pos);
  187. if(name in stripQuotesFromPseudos){
  188. quot = data.charAt(0);
  189. if(quot === data.slice(-1) && quot in quotes){
  190. data = data.slice(1, -1);
  191. }
  192. data = unescapeCSS(data);
  193. }
  194. }
  195. }
  196. tokens.push({type: "pseudo", name: name, data: data});
  197. } else if(re_name.test(selector)){
  198. name = getName();
  199. if(!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)){
  200. name = name.toLowerCase();
  201. }
  202. tokens.push({type: "tag", name: name});
  203. } else {
  204. if(tokens.length && tokens[tokens.length - 1].type === "descendant"){
  205. tokens.pop();
  206. }
  207. addToken(subselects, tokens);
  208. return selector;
  209. }
  210. }
  211. }
  212. addToken(subselects, tokens);
  213. return selector;
  214. }
  215. function addToken(subselects, tokens){
  216. if(subselects.length > 0 && tokens.length === 0){
  217. throw new SyntaxError("empty sub-selector");
  218. }
  219. subselects.push(tokens);
  220. }