123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. var openParentheses = '('.charCodeAt(0);
  2. var closeParentheses = ')'.charCodeAt(0);
  3. var singleQuote = '\''.charCodeAt(0);
  4. var doubleQuote = '"'.charCodeAt(0);
  5. var backslash = '\\'.charCodeAt(0);
  6. var slash = '/'.charCodeAt(0);
  7. var comma = ','.charCodeAt(0);
  8. var colon = ':'.charCodeAt(0);
  9. var star = '*'.charCodeAt(0);
  10. module.exports = function (input) {
  11. var tokens = [];
  12. var value = input;
  13. var next, quote, prev, token, escape, escapePos, whitespacePos;
  14. var pos = 0;
  15. var code = value.charCodeAt(pos);
  16. var max = value.length;
  17. var stack = [{ nodes: tokens }];
  18. var balanced = 0;
  19. var parent;
  20. var name = '';
  21. var before = '';
  22. var after = '';
  23. while (pos < max) {
  24. // Whitespaces
  25. if (code <= 32) {
  26. next = pos;
  27. do {
  28. next += 1;
  29. code = value.charCodeAt(next);
  30. } while (code <= 32);
  31. token = value.slice(pos, next);
  32. prev = tokens[tokens.length - 1];
  33. if (code === closeParentheses && balanced) {
  34. after = token;
  35. } else if (prev && prev.type === 'div') {
  36. prev.after = token;
  37. } else if (code === comma || code === colon || code === slash && value.charCodeAt(next + 1) !== star) {
  38. before = token;
  39. } else {
  40. tokens.push({
  41. type: 'space',
  42. sourceIndex: pos,
  43. value: token
  44. });
  45. }
  46. pos = next;
  47. // Quotes
  48. } else if (code === singleQuote || code === doubleQuote) {
  49. next = pos;
  50. quote = code === singleQuote ? '\'' : '"';
  51. token = {
  52. type: 'string',
  53. sourceIndex: pos,
  54. quote: quote
  55. };
  56. do {
  57. escape = false;
  58. next = value.indexOf(quote, next + 1);
  59. if (~next) {
  60. escapePos = next;
  61. while (value.charCodeAt(escapePos - 1) === backslash) {
  62. escapePos -= 1;
  63. escape = !escape;
  64. }
  65. } else {
  66. value += quote;
  67. next = value.length - 1;
  68. token.unclosed = true;
  69. }
  70. } while (escape);
  71. token.value = value.slice(pos + 1, next);
  72. tokens.push(token);
  73. pos = next + 1;
  74. code = value.charCodeAt(pos);
  75. // Comments
  76. } else if (code === slash && value.charCodeAt(pos + 1) === star) {
  77. token = {
  78. type: 'comment',
  79. sourceIndex: pos
  80. };
  81. next = value.indexOf('*/', pos);
  82. if (next === -1) {
  83. token.unclosed = true;
  84. next = value.length;
  85. }
  86. token.value = value.slice(pos + 2, next);
  87. tokens.push(token);
  88. pos = next + 2;
  89. code = value.charCodeAt(pos);
  90. // Dividers
  91. } else if (code === slash || code === comma || code === colon) {
  92. token = value[pos];
  93. tokens.push({
  94. type: 'div',
  95. sourceIndex: pos - before.length,
  96. value: token,
  97. before: before,
  98. after: ''
  99. });
  100. before = '';
  101. pos += 1;
  102. code = value.charCodeAt(pos);
  103. // Open parentheses
  104. } else if (openParentheses === code) {
  105. // Whitespaces after open parentheses
  106. next = pos;
  107. do {
  108. next += 1;
  109. code = value.charCodeAt(next);
  110. } while (code <= 32);
  111. token = {
  112. type: 'function',
  113. sourceIndex: pos - name.length,
  114. value: name,
  115. before: value.slice(pos + 1, next)
  116. };
  117. pos = next;
  118. if (name === 'url' && code !== singleQuote && code !== doubleQuote) {
  119. next -= 1;
  120. do {
  121. escape = false;
  122. next = value.indexOf(')', next + 1);
  123. if (~next) {
  124. escapePos = next;
  125. while (value.charCodeAt(escapePos - 1) === backslash) {
  126. escapePos -= 1;
  127. escape = !escape;
  128. }
  129. } else {
  130. value += ')';
  131. next = value.length - 1;
  132. token.unclosed = true;
  133. }
  134. } while (escape);
  135. // Whitespaces before closed
  136. whitespacePos = next;
  137. do {
  138. whitespacePos -= 1;
  139. code = value.charCodeAt(whitespacePos);
  140. } while (code <= 32);
  141. if (pos !== whitespacePos + 1) {
  142. token.nodes = [{
  143. type: 'word',
  144. sourceIndex: pos,
  145. value: value.slice(pos, whitespacePos + 1)
  146. }];
  147. } else {
  148. token.nodes = [];
  149. }
  150. if (token.unclosed && whitespacePos + 1 !== next) {
  151. token.after = '';
  152. token.nodes.push({
  153. type: 'space',
  154. sourceIndex: whitespacePos + 1,
  155. value: value.slice(whitespacePos + 1, next)
  156. });
  157. } else {
  158. token.after = value.slice(whitespacePos + 1, next);
  159. }
  160. pos = next + 1;
  161. code = value.charCodeAt(pos);
  162. tokens.push(token);
  163. } else {
  164. balanced += 1;
  165. token.after = '';
  166. tokens.push(token);
  167. stack.push(token);
  168. tokens = token.nodes = [];
  169. parent = token;
  170. }
  171. name = '';
  172. // Close parentheses
  173. } else if (closeParentheses === code && balanced) {
  174. pos += 1;
  175. code = value.charCodeAt(pos);
  176. parent.after = after;
  177. after = '';
  178. balanced -= 1;
  179. stack.pop();
  180. parent = stack[balanced];
  181. tokens = parent.nodes;
  182. // Words
  183. } else {
  184. next = pos;
  185. do {
  186. if (code === backslash) {
  187. next += 1;
  188. }
  189. next += 1;
  190. code = value.charCodeAt(next);
  191. } while (next < max && !(
  192. code <= 32 ||
  193. code === singleQuote ||
  194. code === doubleQuote ||
  195. code === comma ||
  196. code === colon ||
  197. code === slash ||
  198. code === openParentheses ||
  199. code === closeParentheses && balanced
  200. ));
  201. token = value.slice(pos, next);
  202. if (openParentheses === code) {
  203. name = token;
  204. } else {
  205. tokens.push({
  206. type: 'word',
  207. sourceIndex: pos,
  208. value: token
  209. });
  210. }
  211. pos = next;
  212. }
  213. }
  214. for (pos = stack.length - 1; pos; pos -= 1) {
  215. stack[pos].unclosed = true;
  216. }
  217. return stack[0].nodes;
  218. };