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