8-restructRuleset.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. var List = require('css-tree').List;
  2. var walk = require('css-tree').walk;
  3. var utils = require('./utils');
  4. function calcSelectorLength(list) {
  5. var length = 0;
  6. list.each(function(data) {
  7. length += data.id.length + 1;
  8. });
  9. return length - 1;
  10. }
  11. function calcDeclarationsLength(tokens) {
  12. var length = 0;
  13. for (var i = 0; i < tokens.length; i++) {
  14. length += tokens[i].length;
  15. }
  16. return (
  17. length + // declarations
  18. tokens.length - 1 // delimeters
  19. );
  20. }
  21. function processRule(node, item, list) {
  22. var avoidRulesMerge = this.block !== null ? this.block.avoidRulesMerge : false;
  23. var selectors = node.prelude.children;
  24. var block = node.block;
  25. var disallowDownMarkers = Object.create(null);
  26. var allowMergeUp = true;
  27. var allowMergeDown = true;
  28. list.prevUntil(item.prev, function(prev, prevItem) {
  29. var prevBlock = prev.block;
  30. var prevType = prev.type;
  31. if (prevType !== 'Rule') {
  32. var unsafe = utils.unsafeToSkipNode.call(selectors, prev);
  33. if (!unsafe && prevType === 'Atrule' && prevBlock) {
  34. walk(prevBlock, {
  35. visit: 'Rule',
  36. enter: function(node) {
  37. node.prelude.children.each(function(data) {
  38. disallowDownMarkers[data.compareMarker] = true;
  39. });
  40. }
  41. });
  42. }
  43. return unsafe;
  44. }
  45. var prevSelectors = prev.prelude.children;
  46. if (node.pseudoSignature !== prev.pseudoSignature) {
  47. return true;
  48. }
  49. allowMergeDown = !prevSelectors.some(function(selector) {
  50. return selector.compareMarker in disallowDownMarkers;
  51. });
  52. // try prev ruleset if simpleselectors has no equal specifity and element selector
  53. if (!allowMergeDown && !allowMergeUp) {
  54. return true;
  55. }
  56. // try to join by selectors
  57. if (allowMergeUp && utils.isEqualSelectors(prevSelectors, selectors)) {
  58. prevBlock.children.appendList(block.children);
  59. list.remove(item);
  60. return true;
  61. }
  62. // try to join by properties
  63. var diff = utils.compareDeclarations(block.children, prevBlock.children);
  64. // console.log(diff.eq, diff.ne1, diff.ne2);
  65. if (diff.eq.length) {
  66. if (!diff.ne1.length && !diff.ne2.length) {
  67. // equal blocks
  68. if (allowMergeDown) {
  69. utils.addSelectors(selectors, prevSelectors);
  70. list.remove(prevItem);
  71. }
  72. return true;
  73. } else if (!avoidRulesMerge) { /* probably we don't need to prevent those merges for @keyframes
  74. TODO: need to be checked */
  75. if (diff.ne1.length && !diff.ne2.length) {
  76. // prevBlock is subset block
  77. var selectorLength = calcSelectorLength(selectors);
  78. var blockLength = calcDeclarationsLength(diff.eq); // declarations length
  79. if (allowMergeUp && selectorLength < blockLength) {
  80. utils.addSelectors(prevSelectors, selectors);
  81. block.children = new List().fromArray(diff.ne1);
  82. }
  83. } else if (!diff.ne1.length && diff.ne2.length) {
  84. // node is subset of prevBlock
  85. var selectorLength = calcSelectorLength(prevSelectors);
  86. var blockLength = calcDeclarationsLength(diff.eq); // declarations length
  87. if (allowMergeDown && selectorLength < blockLength) {
  88. utils.addSelectors(selectors, prevSelectors);
  89. prevBlock.children = new List().fromArray(diff.ne2);
  90. }
  91. } else {
  92. // diff.ne1.length && diff.ne2.length
  93. // extract equal block
  94. var newSelector = {
  95. type: 'SelectorList',
  96. loc: null,
  97. children: utils.addSelectors(prevSelectors.copy(), selectors)
  98. };
  99. var newBlockLength = calcSelectorLength(newSelector.children) + 2; // selectors length + curly braces length
  100. var blockLength = calcDeclarationsLength(diff.eq); // declarations length
  101. // create new ruleset if declarations length greater than
  102. // ruleset description overhead
  103. if (blockLength >= newBlockLength) {
  104. var newItem = list.createItem({
  105. type: 'Rule',
  106. loc: null,
  107. prelude: newSelector,
  108. block: {
  109. type: 'Block',
  110. loc: null,
  111. children: new List().fromArray(diff.eq)
  112. },
  113. pseudoSignature: node.pseudoSignature
  114. });
  115. block.children = new List().fromArray(diff.ne1);
  116. prevBlock.children = new List().fromArray(diff.ne2overrided);
  117. if (allowMergeUp) {
  118. list.insert(newItem, prevItem);
  119. } else {
  120. list.insert(newItem, item);
  121. }
  122. return true;
  123. }
  124. }
  125. }
  126. }
  127. if (allowMergeUp) {
  128. // TODO: disallow up merge only if any property interception only (i.e. diff.ne2overrided.length > 0);
  129. // await property families to find property interception correctly
  130. allowMergeUp = !prevSelectors.some(function(prevSelector) {
  131. return selectors.some(function(selector) {
  132. return selector.compareMarker === prevSelector.compareMarker;
  133. });
  134. });
  135. }
  136. prevSelectors.each(function(data) {
  137. disallowDownMarkers[data.compareMarker] = true;
  138. });
  139. });
  140. }
  141. module.exports = function restructRule(ast) {
  142. walk(ast, {
  143. visit: 'Rule',
  144. reverse: true,
  145. enter: processRule
  146. });
  147. };