compress.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. if ('restructure' in options) {
  78. return options.restructure;
  79. }
  80. return 'restructuring' in options ? options.restructuring : true;
  81. }
  82. function wrapBlock(block) {
  83. return new List().appendData({
  84. type: 'Rule',
  85. loc: null,
  86. prelude: {
  87. type: 'SelectorList',
  88. loc: null,
  89. children: new List().appendData({
  90. type: 'Selector',
  91. loc: null,
  92. children: new List().appendData({
  93. type: 'TypeSelector',
  94. loc: null,
  95. name: 'x'
  96. })
  97. })
  98. },
  99. block: block
  100. });
  101. }
  102. module.exports = function compress(ast, options) {
  103. ast = ast || { type: 'StyleSheet', loc: null, children: new List() };
  104. options = options || {};
  105. var compressOptions = {
  106. logger: typeof options.logger === 'function' ? options.logger : function() {},
  107. restructuring: getRestructureOption(options),
  108. forceMediaMerge: Boolean(options.forceMediaMerge),
  109. usage: options.usage ? usageUtils.buildIndex(options.usage) : false
  110. };
  111. var specialComments = getCommentsOption(options);
  112. var firstAtrulesAllowed = true;
  113. var input;
  114. var output = new List();
  115. var chunk;
  116. var chunkNum = 1;
  117. var chunkChildren;
  118. if (options.clone) {
  119. ast = clone(ast);
  120. }
  121. if (ast.type === 'StyleSheet') {
  122. input = ast.children;
  123. ast.children = output;
  124. } else {
  125. input = wrapBlock(ast);
  126. }
  127. do {
  128. chunk = readChunk(input, Boolean(specialComments));
  129. compressChunk(chunk.stylesheet, firstAtrulesAllowed, chunkNum++, compressOptions);
  130. chunkChildren = chunk.stylesheet.children;
  131. if (chunk.comment) {
  132. // add \n before comment if there is another content in output
  133. if (!output.isEmpty()) {
  134. output.insert(List.createItem({
  135. type: 'Raw',
  136. value: '\n'
  137. }));
  138. }
  139. output.insert(List.createItem(chunk.comment));
  140. // add \n after comment if chunk is not empty
  141. if (!chunkChildren.isEmpty()) {
  142. output.insert(List.createItem({
  143. type: 'Raw',
  144. value: '\n'
  145. }));
  146. }
  147. }
  148. if (firstAtrulesAllowed && !chunkChildren.isEmpty()) {
  149. var lastRule = chunkChildren.last();
  150. if (lastRule.type !== 'Atrule' ||
  151. (lastRule.name !== 'import' && lastRule.name !== 'charset')) {
  152. firstAtrulesAllowed = false;
  153. }
  154. }
  155. if (specialComments !== 'exclamation') {
  156. specialComments = false;
  157. }
  158. output.appendList(chunkChildren);
  159. } while (!input.isEmpty());
  160. return {
  161. ast: ast
  162. };
  163. };