tokenize.js 8.2 KB

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