restructure.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. var extractProperties = require('./extractor');
  2. var canReorderSingle = require('./reorderable').canReorderSingle;
  3. var stringifyBody = require('../stringifier/one-time').body;
  4. var stringifySelectors = require('../stringifier/one-time').selectors;
  5. var cleanUpSelectorDuplicates = require('./clean-up').selectorDuplicates;
  6. var isSpecial = require('./is-special');
  7. var cloneArray = require('../utils/clone-array');
  8. function naturalSorter(a, b) {
  9. return a > b;
  10. }
  11. function cloneAndMergeSelectors(propertyA, propertyB) {
  12. var cloned = cloneArray(propertyA);
  13. cloned[5] = cloned[5].concat(propertyB[5]);
  14. return cloned;
  15. }
  16. function restructure(tokens, options) {
  17. var movableTokens = {};
  18. var movedProperties = [];
  19. var multiPropertyMoveCache = {};
  20. var movedToBeDropped = [];
  21. var maxCombinationsLevel = 2;
  22. var ID_JOIN_CHARACTER = '%';
  23. function sendToMultiPropertyMoveCache(position, movedProperty, allFits) {
  24. for (var i = allFits.length - 1; i >= 0; i--) {
  25. var fit = allFits[i][0];
  26. var id = addToCache(movedProperty, fit);
  27. if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) {
  28. removeAllMatchingFromCache(id);
  29. break;
  30. }
  31. }
  32. }
  33. function addToCache(movedProperty, fit) {
  34. var id = cacheId(fit);
  35. multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || [];
  36. multiPropertyMoveCache[id].push([movedProperty, fit]);
  37. return id;
  38. }
  39. function removeAllMatchingFromCache(matchId) {
  40. var matchSelectors = matchId.split(ID_JOIN_CHARACTER);
  41. var forRemoval = [];
  42. var i;
  43. for (var id in multiPropertyMoveCache) {
  44. var selectors = id.split(ID_JOIN_CHARACTER);
  45. for (i = selectors.length - 1; i >= 0; i--) {
  46. if (matchSelectors.indexOf(selectors[i]) > -1) {
  47. forRemoval.push(id);
  48. break;
  49. }
  50. }
  51. }
  52. for (i = forRemoval.length - 1; i >= 0; i--) {
  53. delete multiPropertyMoveCache[forRemoval[i]];
  54. }
  55. }
  56. function cacheId(cachedTokens) {
  57. var id = [];
  58. for (var i = 0, l = cachedTokens.length; i < l; i++) {
  59. id.push(stringifySelectors(cachedTokens[i][1]));
  60. }
  61. return id.join(ID_JOIN_CHARACTER);
  62. }
  63. function tokensToMerge(sourceTokens) {
  64. var uniqueTokensWithBody = [];
  65. var mergeableTokens = [];
  66. for (var i = sourceTokens.length - 1; i >= 0; i--) {
  67. if (isSpecial(options, stringifySelectors(sourceTokens[i][1])))
  68. continue;
  69. mergeableTokens.unshift(sourceTokens[i]);
  70. if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
  71. uniqueTokensWithBody.push(sourceTokens[i]);
  72. }
  73. return uniqueTokensWithBody.length > 1 ?
  74. mergeableTokens :
  75. [];
  76. }
  77. function shortenIfPossible(position, movedProperty) {
  78. var name = movedProperty[0];
  79. var value = movedProperty[1];
  80. var key = movedProperty[4];
  81. var valueSize = name.length + value.length + 1;
  82. var allSelectors = [];
  83. var qualifiedTokens = [];
  84. var mergeableTokens = tokensToMerge(movableTokens[key]);
  85. if (mergeableTokens.length < 2)
  86. return;
  87. var allFits = findAllFits(mergeableTokens, valueSize, 1);
  88. var bestFit = allFits[0];
  89. if (bestFit[1] > 0)
  90. return sendToMultiPropertyMoveCache(position, movedProperty, allFits);
  91. for (var i = bestFit[0].length - 1; i >=0; i--) {
  92. allSelectors = bestFit[0][i][1].concat(allSelectors);
  93. qualifiedTokens.unshift(bestFit[0][i]);
  94. }
  95. allSelectors = cleanUpSelectorDuplicates(allSelectors);
  96. dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens);
  97. }
  98. function fitSorter(fit1, fit2) {
  99. return fit1[1] > fit2[1];
  100. }
  101. function findAllFits(mergeableTokens, propertySize, propertiesCount) {
  102. var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1);
  103. return combinations.sort(fitSorter);
  104. }
  105. function allCombinations(tokensVariant, propertySize, propertiesCount, level) {
  106. var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]];
  107. if (tokensVariant.length > 2 && level > 0) {
  108. for (var i = tokensVariant.length - 1; i >= 0; i--) {
  109. var subVariant = Array.prototype.slice.call(tokensVariant, 0);
  110. subVariant.splice(i, 1);
  111. differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1));
  112. }
  113. }
  114. return differenceVariants;
  115. }
  116. function sizeDifference(tokensVariant, propertySize, propertiesCount) {
  117. var allSelectorsSize = 0;
  118. for (var i = tokensVariant.length - 1; i >= 0; i--) {
  119. allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelectors(tokensVariant[i][1]).length : -1;
  120. }
  121. return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
  122. }
  123. function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) {
  124. var i, j, k, m;
  125. var allProperties = [];
  126. for (i = mergeableTokens.length - 1; i >= 0; i--) {
  127. var mergeableToken = mergeableTokens[i];
  128. for (j = mergeableToken[2].length - 1; j >= 0; j--) {
  129. var mergeableProperty = mergeableToken[2][j];
  130. for (k = 0, m = properties.length; k < m; k++) {
  131. var property = properties[k];
  132. var mergeablePropertyName = mergeableProperty[0][0];
  133. var propertyName = property[0];
  134. var propertyBody = property[4];
  135. if (mergeablePropertyName == propertyName && stringifyBody([mergeableProperty]) == propertyBody) {
  136. mergeableToken[2].splice(j, 1);
  137. break;
  138. }
  139. }
  140. }
  141. }
  142. for (i = properties.length - 1; i >= 0; i--) {
  143. allProperties.unshift(properties[i][3]);
  144. }
  145. var newToken = ['selector', allSelectors, allProperties];
  146. tokens.splice(position, 0, newToken);
  147. }
  148. function dropPropertiesAt(position, movedProperty) {
  149. var key = movedProperty[4];
  150. var toMove = movableTokens[key];
  151. if (toMove && toMove.length > 1) {
  152. if (!shortenMultiMovesIfPossible(position, movedProperty))
  153. shortenIfPossible(position, movedProperty);
  154. }
  155. }
  156. function shortenMultiMovesIfPossible(position, movedProperty) {
  157. var candidates = [];
  158. var propertiesAndMergableTokens = [];
  159. var key = movedProperty[4];
  160. var j, k;
  161. var mergeableTokens = tokensToMerge(movableTokens[key]);
  162. if (mergeableTokens.length < 2)
  163. return;
  164. movableLoop:
  165. for (var value in movableTokens) {
  166. var tokensList = movableTokens[value];
  167. for (j = mergeableTokens.length - 1; j >= 0; j--) {
  168. if (tokensList.indexOf(mergeableTokens[j]) == -1)
  169. continue movableLoop;
  170. }
  171. candidates.push(value);
  172. }
  173. if (candidates.length < 2)
  174. return false;
  175. for (j = candidates.length - 1; j >= 0; j--) {
  176. for (k = movedProperties.length - 1; k >= 0; k--) {
  177. if (movedProperties[k][4] == candidates[j]) {
  178. propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]);
  179. break;
  180. }
  181. }
  182. }
  183. return processMultiPropertyMove(position, propertiesAndMergableTokens);
  184. }
  185. function processMultiPropertyMove(position, propertiesAndMergableTokens) {
  186. var valueSize = 0;
  187. var properties = [];
  188. var property;
  189. for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
  190. property = propertiesAndMergableTokens[i][0];
  191. var fullValue = property[4];
  192. valueSize += fullValue.length + (i > 0 ? 1 : 0);
  193. properties.push(property);
  194. }
  195. var mergeableTokens = propertiesAndMergableTokens[0][1];
  196. var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0];
  197. if (bestFit[1] > 0)
  198. return false;
  199. var allSelectors = [];
  200. var qualifiedTokens = [];
  201. for (i = bestFit[0].length - 1; i >= 0; i--) {
  202. allSelectors = bestFit[0][i][1].concat(allSelectors);
  203. qualifiedTokens.unshift(bestFit[0][i]);
  204. }
  205. allSelectors = cleanUpSelectorDuplicates(allSelectors);
  206. dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens);
  207. for (i = properties.length - 1; i >= 0; i--) {
  208. property = properties[i];
  209. var index = movedProperties.indexOf(property);
  210. delete movableTokens[property[4]];
  211. if (index > -1 && movedToBeDropped.indexOf(index) == -1)
  212. movedToBeDropped.push(index);
  213. }
  214. return true;
  215. }
  216. function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) {
  217. var propertyName = property[0];
  218. var movedPropertyName = movedProperty[0];
  219. if (propertyName != movedPropertyName)
  220. return false;
  221. var key = movedProperty[4];
  222. var toMove = movableTokens[key];
  223. return toMove && toMove.indexOf(token) > -1;
  224. }
  225. for (var i = tokens.length - 1; i >= 0; i--) {
  226. var token = tokens[i];
  227. var isSelector;
  228. var j, k, m;
  229. var samePropertyAt;
  230. if (token[0] == 'selector') {
  231. isSelector = true;
  232. } else if (token[0] == 'block') {
  233. isSelector = false;
  234. } else {
  235. continue;
  236. }
  237. // We cache movedProperties.length as it may change in the loop
  238. var movedCount = movedProperties.length;
  239. var properties = extractProperties(token);
  240. movedToBeDropped = [];
  241. var unmovableInCurrentToken = [];
  242. for (j = properties.length - 1; j >= 0; j--) {
  243. for (k = j - 1; k >= 0; k--) {
  244. if (!canReorderSingle(properties[j], properties[k])) {
  245. unmovableInCurrentToken.push(j);
  246. break;
  247. }
  248. }
  249. }
  250. for (j = properties.length - 1; j >= 0; j--) {
  251. var property = properties[j];
  252. var movedSameProperty = false;
  253. for (k = 0; k < movedCount; k++) {
  254. var movedProperty = movedProperties[k];
  255. if (movedToBeDropped.indexOf(k) == -1 && !canReorderSingle(property, movedProperty) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token)) {
  256. dropPropertiesAt(i + 1, movedProperty, token);
  257. if (movedToBeDropped.indexOf(k) == -1) {
  258. movedToBeDropped.push(k);
  259. delete movableTokens[movedProperty[4]];
  260. }
  261. }
  262. if (!movedSameProperty) {
  263. movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1];
  264. if (movedSameProperty) {
  265. samePropertyAt = k;
  266. }
  267. }
  268. }
  269. if (!isSelector || unmovableInCurrentToken.indexOf(j) > -1)
  270. continue;
  271. var key = property[4];
  272. movableTokens[key] = movableTokens[key] || [];
  273. movableTokens[key].push(token);
  274. if (movedSameProperty) {
  275. movedProperties[samePropertyAt] = cloneAndMergeSelectors(movedProperties[samePropertyAt], property);
  276. } else {
  277. movedProperties.push(property);
  278. }
  279. }
  280. movedToBeDropped = movedToBeDropped.sort(naturalSorter);
  281. for (j = 0, m = movedToBeDropped.length; j < m; j++) {
  282. var dropAt = movedToBeDropped[j] - j;
  283. movedProperties.splice(dropAt, 1);
  284. }
  285. }
  286. var position = tokens[0] && tokens[0][0] == 'at-rule' && tokens[0][1][0].indexOf('@charset') === 0 ? 1 : 0;
  287. for (; position < tokens.length - 1; position++) {
  288. var isImportRule = tokens[position][0] === 'at-rule' && tokens[position][1][0].indexOf('@import') === 0;
  289. var isEscapedCommentSpecial = tokens[position][0] === 'text' && tokens[position][1][0].indexOf('__ESCAPED_COMMENT_SPECIAL') === 0;
  290. if (!(isImportRule || isEscapedCommentSpecial))
  291. break;
  292. }
  293. for (i = 0; i < movedProperties.length; i++) {
  294. dropPropertiesAt(position, movedProperties[i]);
  295. }
  296. }
  297. module.exports = restructure;