compress.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. var List = require('css-tree').List;
  2. var clone = require('css-tree').clone;
  3. var usageUtils = require('./usage');
  4. var clean = require('./clean');
  5. var replace = require('./replace');
  6. var restructure = require('./restructure');
  7. var walk = require('css-tree').walk;
  8. function readChunk(children, specialComments) {
  9. var buffer = new List();
  10. var nonSpaceTokenInBuffer = false;
  11. var protectedComment;
  12. children.nextUntil(children.head, function(node, item, list) {
  13. if (node.type === 'Comment') {
  14. if (!specialComments || node.value.charAt(0) !== '!') {
  15. list.remove(item);
  16. return;
  17. }
  18. if (nonSpaceTokenInBuffer || protectedComment) {
  19. return true;
  20. }
  21. list.remove(item);
  22. protectedComment = node;
  23. return;
  24. }
  25. if (node.type !== 'WhiteSpace') {
  26. nonSpaceTokenInBuffer = true;
  27. }
  28. buffer.insert(list.remove(item));
  29. });
  30. return {
  31. comment: protectedComment,
  32. stylesheet: {
  33. type: 'StyleSheet',
  34. loc: null,
  35. children: buffer
  36. }
  37. };
  38. }
  39. function compressChunk(ast, firstAtrulesAllowed, num, options) {
  40. options.logger('Compress block #' + num, null, true);
  41. var seed = 1;
  42. if (ast.type === 'StyleSheet') {
  43. ast.firstAtrulesAllowed = firstAtrulesAllowed;
  44. ast.id = seed++;
  45. }
  46. walk(ast, {
  47. visit: 'Atrule',
  48. enter: function markScopes(node) {
  49. if (node.block !== null) {
  50. node.block.id = seed++;
  51. }
  52. }
  53. });
  54. options.logger('init', ast);
  55. // remove redundant
  56. clean(ast, options);
  57. options.logger('clean', ast);
  58. // replace nodes for shortened forms
  59. replace(ast, options);
  60. options.logger('replace', ast);
  61. // structure optimisations
  62. if (options.restructuring) {
  63. restructure(ast, options);
  64. }
  65. return ast;
  66. }
  67. function getCommentsOption(options) {
  68. var comments = 'comments' in options ? options.comments : 'exclamation';
  69. if (typeof comments === 'boolean') {
  70. comments = comments ? 'exclamation' : false;
  71. } else if (comments !== 'exclamation' && comments !== 'first-exclamation') {
  72. comments = false;
  73. }
  74. return comments;
  75. }
  76. function getRestructureOption(options) {
  77. return 'restructure' in options ? options.restructure :
  78. 'restructuring' in options ? options.restructuring :
  79. true;
  80. }
  81. function wrapBlock(block) {
  82. return new List().appendData({
  83. type: 'Rule',
  84. loc: null,
  85. prelude: {
  86. type: 'SelectorList',
  87. loc: null,
  88. children: new List().appendData({
  89. type: 'Selector',
  90. loc: null,
  91. children: new List().appendData({
  92. type: 'TypeSelector',
  93. loc: null,
  94. name: 'x'
  95. })
  96. })
  97. },
  98. block: block
  99. });
  100. }
  101. module.exports = function compress(ast, options) {
  102. ast = ast || { type: 'StyleSheet', loc: null, children: new List() };
  103. options = options || {};
  104. var compressOptions = {
  105. logger: typeof options.logger === 'function' ? options.logger : function() {},
  106. restructuring: getRestructureOption(options),
  107. forceMediaMerge: Boolean(options.forceMediaMerge),
  108. usage: options.usage ? usageUtils.buildIndex(options.usage) : false
  109. };
  110. var specialComments = getCommentsOption(options);
  111. var firstAtrulesAllowed = true;
  112. var input;
  113. var output = new List();
  114. var chunk;
  115. var chunkNum = 1;
  116. var chunkChildren;
  117. if (options.clone) {
  118. ast = clone(ast);
  119. }
  120. if (ast.type === 'StyleSheet') {
  121. input = ast.children;
  122. ast.children = output;
  123. } else {
  124. input = wrapBlock(ast);
  125. }
  126. do {
  127. chunk = readChunk(input, Boolean(specialComments));
  128. compressChunk(chunk.stylesheet, firstAtrulesAllowed, chunkNum++, compressOptions);
  129. chunkChildren = chunk.stylesheet.children;
  130. if (chunk.comment) {
  131. // add \n before comment if there is another content in output
  132. if (!output.isEmpty()) {
  133. output.insert(List.createItem({
  134. type: 'Raw',
  135. value: '\n'
  136. }));
  137. }
  138. output.insert(List.createItem(chunk.comment));
  139. // add \n after comment if chunk is not empty
  140. if (!chunkChildren.isEmpty()) {
  141. output.insert(List.createItem({
  142. type: 'Raw',
  143. value: '\n'
  144. }));
  145. }
  146. }
  147. if (firstAtrulesAllowed && !chunkChildren.isEmpty()) {
  148. var lastRule = chunkChildren.last();
  149. if (lastRule.type !== 'Atrule' ||
  150. (lastRule.name !== 'import' && lastRule.name !== 'charset')) {
  151. firstAtrulesAllowed = false;
  152. }
  153. }
  154. if (specialComments !== 'exclamation') {
  155. specialComments = false;
  156. }
  157. output.appendList(chunkChildren);
  158. } while (!input.isEmpty());
  159. return {
  160. ast: ast
  161. };
  162. };