var canReorderSingle = require('./reorderable').canReorderSingle; var extractProperties = require('./extract-properties'); var isMergeable = require('./is-mergeable'); var tidyRuleDuplicates = require('./tidy-rule-duplicates'); var Token = require('../../tokenizer/token'); var cloneArray = require('../../utils/clone-array'); var serializeBody = require('../../writer/one-time').body; var serializeRules = require('../../writer/one-time').rules; function naturalSorter(a, b) { return a > b ? 1 : -1; } function cloneAndMergeSelectors(propertyA, propertyB) { var cloned = cloneArray(propertyA); cloned[5] = cloned[5].concat(propertyB[5]); return cloned; } function restructure(tokens, context) { var options = context.options; var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses; var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements; var mergeLimit = options.compatibility.selectors.mergeLimit; var multiplePseudoMerging = options.compatibility.selectors.multiplePseudoMerging; var specificityCache = context.cache.specificity; var movableTokens = {}; var movedProperties = []; var multiPropertyMoveCache = {}; var movedToBeDropped = []; var maxCombinationsLevel = 2; var ID_JOIN_CHARACTER = '%'; function sendToMultiPropertyMoveCache(position, movedProperty, allFits) { for (var i = allFits.length - 1; i >= 0; i--) { var fit = allFits[i][0]; var id = addToCache(movedProperty, fit); if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) { removeAllMatchingFromCache(id); break; } } } function addToCache(movedProperty, fit) { var id = cacheId(fit); multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || []; multiPropertyMoveCache[id].push([movedProperty, fit]); return id; } function removeAllMatchingFromCache(matchId) { var matchSelectors = matchId.split(ID_JOIN_CHARACTER); var forRemoval = []; var i; for (var id in multiPropertyMoveCache) { var selectors = id.split(ID_JOIN_CHARACTER); for (i = selectors.length - 1; i >= 0; i--) { if (matchSelectors.indexOf(selectors[i]) > -1) { forRemoval.push(id); break; } } } for (i = forRemoval.length - 1; i >= 0; i--) { delete multiPropertyMoveCache[forRemoval[i]]; } } function cacheId(cachedTokens) { var id = []; for (var i = 0, l = cachedTokens.length; i < l; i++) { id.push(serializeRules(cachedTokens[i][1])); } return id.join(ID_JOIN_CHARACTER); } function tokensToMerge(sourceTokens) { var uniqueTokensWithBody = []; var mergeableTokens = []; for (var i = sourceTokens.length - 1; i >= 0; i--) { if (!isMergeable(serializeRules(sourceTokens[i][1]), mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging)) { continue; } mergeableTokens.unshift(sourceTokens[i]); if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1) uniqueTokensWithBody.push(sourceTokens[i]); } return uniqueTokensWithBody.length > 1 ? mergeableTokens : []; } function shortenIfPossible(position, movedProperty) { var name = movedProperty[0]; var value = movedProperty[1]; var key = movedProperty[4]; var valueSize = name.length + value.length + 1; var allSelectors = []; var qualifiedTokens = []; var mergeableTokens = tokensToMerge(movableTokens[key]); if (mergeableTokens.length < 2) return; var allFits = findAllFits(mergeableTokens, valueSize, 1); var bestFit = allFits[0]; if (bestFit[1] > 0) return sendToMultiPropertyMoveCache(position, movedProperty, allFits); for (var i = bestFit[0].length - 1; i >=0; i--) { allSelectors = bestFit[0][i][1].concat(allSelectors); qualifiedTokens.unshift(bestFit[0][i]); } allSelectors = tidyRuleDuplicates(allSelectors); dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens); } function fitSorter(fit1, fit2) { return fit1[1] > fit2[1] ? 1 : (fit1[1] == fit2[1] ? 0 : -1); } function findAllFits(mergeableTokens, propertySize, propertiesCount) { var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1); return combinations.sort(fitSorter); } function allCombinations(tokensVariant, propertySize, propertiesCount, level) { var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]]; if (tokensVariant.length > 2 && level > 0) { for (var i = tokensVariant.length - 1; i >= 0; i--) { var subVariant = Array.prototype.slice.call(tokensVariant, 0); subVariant.splice(i, 1); differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1)); } } return differenceVariants; } function sizeDifference(tokensVariant, propertySize, propertiesCount) { var allSelectorsSize = 0; for (var i = tokensVariant.length - 1; i >= 0; i--) { allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? serializeRules(tokensVariant[i][1]).length : -1; } return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1; } function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) { var i, j, k, m; var allProperties = []; for (i = mergeableTokens.length - 1; i >= 0; i--) { var mergeableToken = mergeableTokens[i]; for (j = mergeableToken[2].length - 1; j >= 0; j--) { var mergeableProperty = mergeableToken[2][j]; for (k = 0, m = properties.length; k < m; k++) { var property = properties[k]; var mergeablePropertyName = mergeableProperty[1][1]; var propertyName = property[0]; var propertyBody = property[4]; if (mergeablePropertyName == propertyName && serializeBody([mergeableProperty]) == propertyBody) { mergeableToken[2].splice(j, 1); break; } } } } for (i = properties.length - 1; i >= 0; i--) { allProperties.unshift(properties[i][3]); } var newToken = [Token.RULE, allSelectors, allProperties]; tokens.splice(position, 0, newToken); } function dropPropertiesAt(position, movedProperty) { var key = movedProperty[4]; var toMove = movableTokens[key]; if (toMove && toMove.length > 1) { if (!shortenMultiMovesIfPossible(position, movedProperty)) shortenIfPossible(position, movedProperty); } } function shortenMultiMovesIfPossible(position, movedProperty) { var candidates = []; var propertiesAndMergableTokens = []; var key = movedProperty[4]; var j, k; var mergeableTokens = tokensToMerge(movableTokens[key]); if (mergeableTokens.length < 2) return; movableLoop: for (var value in movableTokens) { var tokensList = movableTokens[value]; for (j = mergeableTokens.length - 1; j >= 0; j--) { if (tokensList.indexOf(mergeableTokens[j]) == -1) continue movableLoop; } candidates.push(value); } if (candidates.length < 2) return false; for (j = candidates.length - 1; j >= 0; j--) { for (k = movedProperties.length - 1; k >= 0; k--) { if (movedProperties[k][4] == candidates[j]) { propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]); break; } } } return processMultiPropertyMove(position, propertiesAndMergableTokens); } function processMultiPropertyMove(position, propertiesAndMergableTokens) { var valueSize = 0; var properties = []; var property; for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) { property = propertiesAndMergableTokens[i][0]; var fullValue = property[4]; valueSize += fullValue.length + (i > 0 ? 1 : 0); properties.push(property); } var mergeableTokens = propertiesAndMergableTokens[0][1]; var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0]; if (bestFit[1] > 0) return false; var allSelectors = []; var qualifiedTokens = []; for (i = bestFit[0].length - 1; i >= 0; i--) { allSelectors = bestFit[0][i][1].concat(allSelectors); qualifiedTokens.unshift(bestFit[0][i]); } allSelectors = tidyRuleDuplicates(allSelectors); dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens); for (i = properties.length - 1; i >= 0; i--) { property = properties[i]; var index = movedProperties.indexOf(property); delete movableTokens[property[4]]; if (index > -1 && movedToBeDropped.indexOf(index) == -1) movedToBeDropped.push(index); } return true; } function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) { var propertyName = property[0]; var movedPropertyName = movedProperty[0]; if (propertyName != movedPropertyName) return false; var key = movedProperty[4]; var toMove = movableTokens[key]; return toMove && toMove.indexOf(token) > -1; } for (var i = tokens.length - 1; i >= 0; i--) { var token = tokens[i]; var isRule; var j, k, m; var samePropertyAt; if (token[0] == Token.RULE) { isRule = true; } else if (token[0] == Token.NESTED_BLOCK) { isRule = false; } else { continue; } // We cache movedProperties.length as it may change in the loop var movedCount = movedProperties.length; var properties = extractProperties(token); movedToBeDropped = []; var unmovableInCurrentToken = []; for (j = properties.length - 1; j >= 0; j--) { for (k = j - 1; k >= 0; k--) { if (!canReorderSingle(properties[j], properties[k], specificityCache)) { unmovableInCurrentToken.push(j); break; } } } for (j = properties.length - 1; j >= 0; j--) { var property = properties[j]; var movedSameProperty = false; for (k = 0; k < movedCount; k++) { var movedProperty = movedProperties[k]; if (movedToBeDropped.indexOf(k) == -1 && (!canReorderSingle(property, movedProperty, specificityCache) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) || movableTokens[movedProperty[4]] && movableTokens[movedProperty[4]].length === mergeLimit)) { dropPropertiesAt(i + 1, movedProperty); if (movedToBeDropped.indexOf(k) == -1) { movedToBeDropped.push(k); delete movableTokens[movedProperty[4]]; } } if (!movedSameProperty) { movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1]; if (movedSameProperty) { samePropertyAt = k; } } } if (!isRule || unmovableInCurrentToken.indexOf(j) > -1) continue; var key = property[4]; if (movedSameProperty && movedProperties[samePropertyAt][5].length + property[5].length > mergeLimit) { dropPropertiesAt(i + 1, movedProperties[samePropertyAt]); movedProperties.splice(samePropertyAt, 1); movableTokens[key] = [token]; movedSameProperty = false; } else { movableTokens[key] = movableTokens[key] || []; movableTokens[key].push(token); } if (movedSameProperty) { movedProperties[samePropertyAt] = cloneAndMergeSelectors(movedProperties[samePropertyAt], property); } else { movedProperties.push(property); } } movedToBeDropped = movedToBeDropped.sort(naturalSorter); for (j = 0, m = movedToBeDropped.length; j < m; j++) { var dropAt = movedToBeDropped[j] - j; movedProperties.splice(dropAt, 1); } } var position = tokens[0] && tokens[0][0] == Token.AT_RULE && tokens[0][1].indexOf('@charset') === 0 ? 1 : 0; for (; position < tokens.length - 1; position++) { var isImportRule = tokens[position][0] === Token.AT_RULE && tokens[position][1].indexOf('@import') === 0; var isComment = tokens[position][0] === Token.COMMENT; if (!(isImportRule || isComment)) break; } for (i = 0; i < movedProperties.length; i++) { dropPropertiesAt(position, movedProperties[i]); } } module.exports = restructure;