393 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			393 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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;
 |