restructure.js 12 KB

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