translateWithSourceMap.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. var SourceMapGenerator = require('source-map').SourceMapGenerator;
  2. var SourceNode = require('source-map').SourceNode;
  3. // Our own implementation of SourceNode#toStringWithSourceMap,
  4. // since SourceNode doesn't allow multiple references to original source.
  5. // Also, as we know structure of result we could be optimize generation
  6. // (currently it's ~40% faster).
  7. function walk(node, fn) {
  8. for (var chunk, i = 0; i < node.children.length; i++) {
  9. chunk = node.children[i];
  10. if (chunk instanceof SourceNode) {
  11. // this is a hack, because source maps doesn't support for 1(generated):N(original)
  12. // if (chunk.merged) {
  13. // fn('', chunk);
  14. // }
  15. walk(chunk, fn);
  16. } else {
  17. fn(chunk, node);
  18. }
  19. }
  20. }
  21. function generateSourceMap(root) {
  22. var map = new SourceMapGenerator();
  23. var css = '';
  24. var sourceMappingActive = false;
  25. var lastOriginalLine = null;
  26. var lastOriginalColumn = null;
  27. var lastIndexOfNewline;
  28. var generated = {
  29. line: 1,
  30. column: 0
  31. };
  32. var activatedMapping = {
  33. generated: generated
  34. };
  35. walk(root, function(chunk, original) {
  36. if (original.line !== null &&
  37. original.column !== null) {
  38. if (lastOriginalLine !== original.line ||
  39. lastOriginalColumn !== original.column) {
  40. map.addMapping({
  41. source: original.source,
  42. original: original,
  43. generated: generated
  44. });
  45. }
  46. lastOriginalLine = original.line;
  47. lastOriginalColumn = original.column;
  48. sourceMappingActive = true;
  49. } else if (sourceMappingActive) {
  50. map.addMapping(activatedMapping);
  51. sourceMappingActive = false;
  52. }
  53. css += chunk;
  54. lastIndexOfNewline = chunk.lastIndexOf('\n');
  55. if (lastIndexOfNewline !== -1) {
  56. generated.line += chunk.match(/\n/g).length;
  57. generated.column = chunk.length - lastIndexOfNewline - 1;
  58. } else {
  59. generated.column += chunk.length;
  60. }
  61. });
  62. return {
  63. css: css,
  64. map: map
  65. };
  66. }
  67. function createAnonymousSourceNode(children) {
  68. return new SourceNode(
  69. null,
  70. null,
  71. null,
  72. children
  73. );
  74. }
  75. function createSourceNode(info, children) {
  76. if (info.primary) {
  77. // special marker node to add several references to original
  78. // var merged = createSourceNode(info.merged, []);
  79. // merged.merged = true;
  80. // children.unshift(merged);
  81. // use recursion, because primary can also has a primary/merged info
  82. return createSourceNode(info.primary, children);
  83. }
  84. return new SourceNode(
  85. info.line,
  86. info.column - 1,
  87. info.source,
  88. children
  89. );
  90. }
  91. function each(list) {
  92. if (list.head === null) {
  93. return '';
  94. }
  95. if (list.head === list.tail) {
  96. return translate(list.head.data);
  97. }
  98. return list.map(translate).join('');
  99. }
  100. function eachDelim(list, delimeter) {
  101. if (list.head === null) {
  102. return '';
  103. }
  104. if (list.head === list.tail) {
  105. return translate(list.head.data);
  106. }
  107. return list.map(translate).join(delimeter);
  108. }
  109. function translate(node) {
  110. switch (node.type) {
  111. case 'StyleSheet':
  112. return createAnonymousSourceNode(node.rules.map(translate));
  113. case 'Atrule':
  114. var nodes = ['@', node.name];
  115. if (node.expression && !node.expression.sequence.isEmpty()) {
  116. nodes.push(' ', translate(node.expression));
  117. }
  118. if (node.block) {
  119. nodes.push('{', translate(node.block), '}');
  120. } else {
  121. nodes.push(';');
  122. }
  123. return createSourceNode(node.info, nodes);
  124. case 'Ruleset':
  125. return createAnonymousSourceNode([
  126. translate(node.selector), '{', translate(node.block), '}'
  127. ]);
  128. case 'Selector':
  129. return createAnonymousSourceNode(node.selectors.map(translate)).join(',');
  130. case 'SimpleSelector':
  131. var nodes = node.sequence.map(function(node) {
  132. // add extra spaces around /deep/ combinator since comment beginning/ending may to be produced
  133. if (node.type === 'Combinator' && node.name === '/deep/') {
  134. return ' ' + translate(node) + ' ';
  135. }
  136. return translate(node);
  137. });
  138. return createSourceNode(node.info, nodes);
  139. case 'Block':
  140. return createAnonymousSourceNode(node.declarations.map(translate)).join(';');
  141. case 'Declaration':
  142. return createSourceNode(
  143. node.info,
  144. [translate(node.property), ':', translate(node.value)]
  145. );
  146. case 'Property':
  147. return node.name;
  148. case 'Value':
  149. return node.important
  150. ? each(node.sequence) + '!important'
  151. : each(node.sequence);
  152. case 'Attribute':
  153. var result = translate(node.name);
  154. var flagsPrefix = ' ';
  155. if (node.operator !== null) {
  156. result += node.operator;
  157. if (node.value !== null) {
  158. result += translate(node.value);
  159. // space between string and flags is not required
  160. if (node.value.type === 'String') {
  161. flagsPrefix = '';
  162. }
  163. }
  164. }
  165. if (node.flags !== null) {
  166. result += flagsPrefix + node.flags;
  167. }
  168. return '[' + result + ']';
  169. case 'FunctionalPseudo':
  170. return ':' + node.name + '(' + eachDelim(node.arguments, ',') + ')';
  171. case 'Function':
  172. return node.name + '(' + eachDelim(node.arguments, ',') + ')';
  173. case 'Negation':
  174. return ':not(' + eachDelim(node.sequence, ',') + ')';
  175. case 'Braces':
  176. return node.open + each(node.sequence) + node.close;
  177. case 'Argument':
  178. case 'AtruleExpression':
  179. return each(node.sequence);
  180. case 'Url':
  181. return 'url(' + translate(node.value) + ')';
  182. case 'Progid':
  183. return translate(node.value);
  184. case 'Combinator':
  185. return node.name;
  186. case 'Identifier':
  187. return node.name;
  188. case 'PseudoClass':
  189. return ':' + node.name;
  190. case 'PseudoElement':
  191. return '::' + node.name;
  192. case 'Class':
  193. return '.' + node.name;
  194. case 'Id':
  195. return '#' + node.name;
  196. case 'Hash':
  197. return '#' + node.value;
  198. case 'Dimension':
  199. return node.value + node.unit;
  200. case 'Nth':
  201. return node.value;
  202. case 'Number':
  203. return node.value;
  204. case 'String':
  205. return node.value;
  206. case 'Operator':
  207. return node.value;
  208. case 'Raw':
  209. return node.value;
  210. case 'Unknown':
  211. return node.value;
  212. case 'Percentage':
  213. return node.value + '%';
  214. case 'Space':
  215. return ' ';
  216. case 'Comment':
  217. return '/*' + node.value + '*/';
  218. default:
  219. throw new Error('Unknown node type: ' + node.type);
  220. }
  221. }
  222. module.exports = function(node) {
  223. return generateSourceMap(
  224. createAnonymousSourceNode(translate(node))
  225. );
  226. };