tokenize.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. var extractProperties = require('./extract-properties');
  2. var extractSelectors = require('./extract-selectors');
  3. var track = require('../source-maps/track');
  4. var split = require('../utils/split');
  5. var path = require('path');
  6. var flatBlock = /(@(font\-face|page|\-ms\-viewport|\-o\-viewport|viewport|counter\-style)|\\@.+?)/;
  7. var BACKSLASH = '\\';
  8. function tokenize(data, outerContext) {
  9. var chunks = split(normalize(data), '}', true, '{', '}');
  10. if (chunks.length === 0)
  11. return [];
  12. var context = {
  13. chunk: chunks.shift(),
  14. chunks: chunks,
  15. column: 0,
  16. cursor: 0,
  17. line: 1,
  18. mode: 'top',
  19. resolvePath: outerContext.options.explicitTarget ?
  20. relativePathResolver(outerContext.options.root, outerContext.options.target) :
  21. null,
  22. source: undefined,
  23. sourceMap: outerContext.options.sourceMap,
  24. sourceMapInlineSources: outerContext.options.sourceMapInlineSources,
  25. sourceMapTracker: outerContext.inputSourceMapTracker,
  26. sourceReader: outerContext.sourceReader,
  27. sourceTracker: outerContext.sourceTracker,
  28. state: [],
  29. track: outerContext.options.sourceMap ?
  30. function (data, snapshotMetadata, fallbacks) { return [[track(data, context, snapshotMetadata, fallbacks)]]; } :
  31. function () { return []; },
  32. warnings: outerContext.warnings
  33. };
  34. return intoTokens(context);
  35. }
  36. function normalize(data) {
  37. return data.replace(/\r\n/g, '\n');
  38. }
  39. function relativePathResolver(root, target) {
  40. var rebaseTo = path.relative(root, target);
  41. return function (relativeTo, sourcePath) {
  42. return relativeTo != sourcePath ?
  43. path.normalize(path.join(path.relative(rebaseTo, path.dirname(relativeTo)), sourcePath)) :
  44. sourcePath;
  45. };
  46. }
  47. function whatsNext(context) {
  48. var mode = context.mode;
  49. var chunk = context.chunk;
  50. var closest;
  51. if (chunk.length == context.cursor) {
  52. if (context.chunks.length === 0)
  53. return null;
  54. context.chunk = chunk = context.chunks.shift();
  55. context.cursor = 0;
  56. }
  57. if (mode == 'body') {
  58. if (chunk[context.cursor] == '}')
  59. return [context.cursor, 'bodyEnd'];
  60. if (chunk.indexOf('}', context.cursor) == -1)
  61. return null;
  62. closest = context.cursor + split(chunk.substring(context.cursor - 1), '}', true, '{', '}')[0].length - 2;
  63. return [closest, 'bodyEnd'];
  64. }
  65. var nextSpecial = nextAt(context, '@');
  66. var nextEscape = chunk.indexOf('__ESCAPED_', context.cursor);
  67. var nextBodyStart = nextAt(context, '{');
  68. var nextBodyEnd = nextAt(context, '}');
  69. if (nextSpecial > -1 && context.cursor > 0 && !/\s|\{|\}|\/|_|,|;/.test(chunk.substring(nextSpecial - 1, nextSpecial))) {
  70. nextSpecial = -1;
  71. }
  72. if (nextEscape > -1 && /\S/.test(chunk.substring(context.cursor, nextEscape)))
  73. nextEscape = -1;
  74. closest = nextSpecial;
  75. if (closest == -1 || (nextEscape > -1 && nextEscape < closest))
  76. closest = nextEscape;
  77. if (closest == -1 || (nextBodyStart > -1 && nextBodyStart < closest))
  78. closest = nextBodyStart;
  79. if (closest == -1 || (nextBodyEnd > -1 && nextBodyEnd < closest))
  80. closest = nextBodyEnd;
  81. if (closest == -1)
  82. return;
  83. if (nextEscape === closest)
  84. return [closest, 'escape'];
  85. if (nextBodyStart === closest)
  86. return [closest, 'bodyStart'];
  87. if (nextBodyEnd === closest)
  88. return [closest, 'bodyEnd'];
  89. if (nextSpecial === closest)
  90. return [closest, 'special'];
  91. }
  92. function nextAt(context, character) {
  93. var startAt = context.cursor;
  94. var chunk = context.chunk;
  95. var position;
  96. while ((position = chunk.indexOf(character, startAt)) > -1) {
  97. if (isEscaped(chunk, position)) {
  98. startAt = position + 1;
  99. } else {
  100. return position;
  101. }
  102. }
  103. return -1;
  104. }
  105. function isEscaped(chunk, position) {
  106. var startAt = position;
  107. var backslashCount = 0;
  108. while (startAt > 0 && chunk[startAt - 1] == BACKSLASH) {
  109. backslashCount++;
  110. startAt--;
  111. }
  112. return backslashCount % 2 !== 0;
  113. }
  114. function intoTokens(context) {
  115. var chunk = context.chunk;
  116. var tokenized = [];
  117. var newToken;
  118. var value;
  119. while (true) {
  120. var next = whatsNext(context);
  121. if (!next) {
  122. var whatsLeft = context.chunk.substring(context.cursor);
  123. if (whatsLeft.trim().length > 0) {
  124. if (context.mode == 'body') {
  125. context.warnings.push('Missing \'}\' after \'' + whatsLeft + '\'. Ignoring.');
  126. } else {
  127. tokenized.push(['text', [whatsLeft]]);
  128. }
  129. context.cursor += whatsLeft.length;
  130. }
  131. break;
  132. }
  133. var nextSpecial = next[0];
  134. var what = next[1];
  135. var nextEnd;
  136. var oldMode;
  137. chunk = context.chunk;
  138. if (context.cursor != nextSpecial && what != 'bodyEnd') {
  139. var spacing = chunk.substring(context.cursor, nextSpecial);
  140. var leadingWhitespace = /^\s+/.exec(spacing);
  141. if (leadingWhitespace) {
  142. context.cursor += leadingWhitespace[0].length;
  143. context.track(leadingWhitespace[0]);
  144. }
  145. }
  146. if (what == 'special') {
  147. var firstOpenBraceAt = chunk.indexOf('{', nextSpecial);
  148. var firstSemicolonAt = chunk.indexOf(';', nextSpecial);
  149. var isSingle = firstSemicolonAt > -1 && (firstOpenBraceAt == -1 || firstSemicolonAt < firstOpenBraceAt);
  150. var isBroken = firstOpenBraceAt == -1 && firstSemicolonAt == -1;
  151. if (isBroken) {
  152. context.warnings.push('Broken declaration: \'' + chunk.substring(context.cursor) + '\'.');
  153. context.cursor = chunk.length;
  154. } else if (isSingle) {
  155. nextEnd = chunk.indexOf(';', nextSpecial + 1);
  156. value = chunk.substring(context.cursor, nextEnd + 1);
  157. tokenized.push([
  158. 'at-rule',
  159. [value].concat(context.track(value, true))
  160. ]);
  161. context.track(';');
  162. context.cursor = nextEnd + 1;
  163. } else {
  164. nextEnd = chunk.indexOf('{', nextSpecial + 1);
  165. value = chunk.substring(context.cursor, nextEnd);
  166. var trimmedValue = value.trim();
  167. var isFlat = flatBlock.test(trimmedValue);
  168. oldMode = context.mode;
  169. context.cursor = nextEnd + 1;
  170. context.mode = isFlat ? 'body' : 'block';
  171. newToken = [
  172. isFlat ? 'flat-block' : 'block'
  173. ];
  174. newToken.push([trimmedValue].concat(context.track(value, true)));
  175. context.track('{');
  176. newToken.push(intoTokens(context));
  177. if (typeof newToken[2] == 'string')
  178. newToken[2] = extractProperties(newToken[2], [[trimmedValue]], context);
  179. context.mode = oldMode;
  180. context.track('}');
  181. tokenized.push(newToken);
  182. }
  183. } else if (what == 'escape') {
  184. nextEnd = chunk.indexOf('__', nextSpecial + 1);
  185. var escaped = chunk.substring(context.cursor, nextEnd + 2);
  186. var isStartSourceMarker = !!context.sourceTracker.nextStart(escaped);
  187. var isEndSourceMarker = !!context.sourceTracker.nextEnd(escaped);
  188. if (isStartSourceMarker) {
  189. context.track(escaped);
  190. context.state.push({
  191. source: context.source,
  192. line: context.line,
  193. column: context.column
  194. });
  195. context.source = context.sourceTracker.nextStart(escaped).filename;
  196. context.line = 1;
  197. context.column = 0;
  198. } else if (isEndSourceMarker) {
  199. var oldState = context.state.pop();
  200. context.source = oldState.source;
  201. context.line = oldState.line;
  202. context.column = oldState.column;
  203. context.track(escaped);
  204. } else {
  205. if (escaped.indexOf('__ESCAPED_COMMENT_SPECIAL') === 0)
  206. tokenized.push(['text', [escaped]]);
  207. context.track(escaped);
  208. }
  209. context.cursor = nextEnd + 2;
  210. } else if (what == 'bodyStart') {
  211. var selectors = extractSelectors(chunk.substring(context.cursor, nextSpecial), context);
  212. oldMode = context.mode;
  213. context.cursor = nextSpecial + 1;
  214. context.mode = 'body';
  215. var body = extractProperties(intoTokens(context), selectors, context);
  216. context.track('{');
  217. context.mode = oldMode;
  218. tokenized.push([
  219. 'selector',
  220. selectors,
  221. body
  222. ]);
  223. } else if (what == 'bodyEnd') {
  224. // extra closing brace at the top level can be safely ignored
  225. if (context.mode == 'top') {
  226. var at = context.cursor;
  227. var warning = chunk[context.cursor] == '}' ?
  228. 'Unexpected \'}\' in \'' + chunk.substring(at - 20, at + 20) + '\'. Ignoring.' :
  229. 'Unexpected content: \'' + chunk.substring(at, nextSpecial + 1) + '\'. Ignoring.';
  230. context.warnings.push(warning);
  231. context.cursor = nextSpecial + 1;
  232. continue;
  233. }
  234. if (context.mode == 'block')
  235. context.track(chunk.substring(context.cursor, nextSpecial));
  236. if (context.mode != 'block')
  237. tokenized = chunk.substring(context.cursor, nextSpecial);
  238. context.cursor = nextSpecial + 1;
  239. break;
  240. }
  241. }
  242. return tokenized;
  243. }
  244. module.exports = tokenize;