// Compacts the tokens by transforming properties into their shorthand notations when possible module.exports = (function () { var isHackValue = function (t) { return t.value === '__hack'; }; var compactShorthands = function(tokens, isImportant, processable, Token) { // Contains the components found so far, grouped by shorthand name var componentsSoFar = { }; // Initializes a prop in componentsSoFar var initSoFar = function (shprop, last, clearAll) { var found = {}; var shorthandPosition; if (!clearAll && componentsSoFar[shprop]) { for (var i = 0; i < processable[shprop].components.length; i++) { var prop = processable[shprop].components[i]; found[prop] = []; if (!(componentsSoFar[shprop].found[prop])) continue; for (var ii = 0; ii < componentsSoFar[shprop].found[prop].length; ii++) { var comp = componentsSoFar[shprop].found[prop][ii]; if (comp.isMarkedForDeletion) continue; found[prop].push(comp); if (comp.position && (!shorthandPosition || comp.position < shorthandPosition)) shorthandPosition = comp.position; } } } componentsSoFar[shprop] = { lastShorthand: last, found: found, shorthandPosition: shorthandPosition }; }; // Adds a component to componentsSoFar var addComponentSoFar = function (token, index) { var shprop = processable[token.prop].componentOf; if (!componentsSoFar[shprop]) initSoFar(shprop); if (!componentsSoFar[shprop].found[token.prop]) componentsSoFar[shprop].found[token.prop] = []; // Add the newfound component to componentsSoFar componentsSoFar[shprop].found[token.prop].push(token); if (!componentsSoFar[shprop].shorthandPosition && index) { // If the haven't decided on where the shorthand should go, put it in the place of this component componentsSoFar[shprop].shorthandPosition = index; } }; // Tries to compact a prop in componentsSoFar var compactSoFar = function (prop) { var i; var componentsCount = processable[prop].components.length; // Check basics if (!componentsSoFar[prop] || !componentsSoFar[prop].found) return false; // Find components for the shorthand var components = []; var realComponents = []; for (i = 0 ; i < componentsCount; i++) { // Get property name var pp = processable[prop].components[i]; if (componentsSoFar[prop].found[pp] && componentsSoFar[prop].found[pp].length) { // We really found it var foundRealComp = componentsSoFar[prop].found[pp][0]; components.push(foundRealComp); if (foundRealComp.isReal !== false) { realComponents.push(foundRealComp); } } else if (componentsSoFar[prop].lastShorthand) { // It's defined in the previous shorthand var c = componentsSoFar[prop].lastShorthand.components[i].clone(isImportant); components.push(c); } else { // Couldn't find this component at all return false; } } if (realComponents.length === 0) { // Couldn't find enough components, sorry return false; } if (realComponents.length === componentsCount) { // When all the components are from real values, only allow shorthanding if their understandability allows it // This is the case when every component can override their default values, or when all of them use the same function var canOverrideDefault = true; var functionNameMatches = true; var functionName; for (var ci = 0; ci < realComponents.length; ci++) { var rc = realComponents[ci]; if (!processable[rc.prop].canOverride(processable[rc.prop].defaultValue, rc.value)) { canOverrideDefault = false; } var iop = rc.value.indexOf('('); if (iop >= 0) { var otherFunctionName = rc.value.substring(0, iop); if (functionName) functionNameMatches = functionNameMatches && otherFunctionName === functionName; else functionName = otherFunctionName; } } if (!canOverrideDefault || !functionNameMatches) return false; } // Compact the components into a shorthand var compacted = processable[prop].putTogether(prop, components, isImportant); if (!(compacted instanceof Array)) { compacted = [compacted]; } var compactedLength = Token.getDetokenizedLength(compacted); var authenticLength = Token.getDetokenizedLength(realComponents); if (realComponents.length === componentsCount || compactedLength < authenticLength || components.some(isHackValue)) { compacted[0].isShorthand = true; compacted[0].components = processable[prop].breakUp(compacted[0]); // Mark the granular components for deletion for (i = 0; i < realComponents.length; i++) { realComponents[i].isMarkedForDeletion = true; } // Mark the position of the new shorthand tokens[componentsSoFar[prop].shorthandPosition].replaceWith = compacted; // Reinitialize the thing for further compacting initSoFar(prop, compacted[0]); for (i = 1; i < compacted.length; i++) { addComponentSoFar(compacted[i]); } // Yes, we can keep the new shorthand! return true; } return false; }; // Tries to compact all properties currently in componentsSoFar var compactAllSoFar = function () { for (var i in componentsSoFar) { if (componentsSoFar.hasOwnProperty(i)) { while (compactSoFar(i)) { } } } }; var i, token; // Go through each token and collect components for each shorthand as we go on for (i = 0; i < tokens.length; i++) { token = tokens[i]; if (token.isMarkedForDeletion) { continue; } if (!processable[token.prop]) { // We don't know what it is, move on continue; } if (processable[token.prop].isShorthand) { // Found an instance of a full shorthand // NOTE: we should NOT mix together tokens that come before and after the shorthands if (token.isImportant === isImportant || (token.isImportant && !isImportant)) { // Try to compact what we've found so far while (compactSoFar(token.prop)) { } // Reset initSoFar(token.prop, token, true); } // TODO: when the old optimizer is removed, take care of this corner case: // div{background-color:#111;background-image:url(aaa);background:linear-gradient(aaa);background-repeat:no-repeat;background-position:1px 2px;background-attachment:scroll} // -> should not be shorthanded / minified at all because the result wouldn't be equivalent to the original in any browser } else if (processable[token.prop].componentOf) { // Found a component of a shorthand if (token.isImportant === isImportant) { // Same importantness token.position = i; addComponentSoFar(token, i); } else if (!isImportant && token.isImportant) { // Use importants for optimalization opportunities // https://github.com/GoalSmashers/clean-css/issues/184 var importantTrickComp = new Token(token.prop, token.value, isImportant); importantTrickComp.isIrrelevant = true; importantTrickComp.isReal = false; addComponentSoFar(importantTrickComp); } } else { // This is not a shorthand and not a component, don't care about it continue; } } // Perform all possible compactions compactAllSoFar(); // Process the results - throw away stuff marked for deletion, insert compacted things, etc. var result = []; for (i = 0; i < tokens.length; i++) { token = tokens[i]; if (token.replaceWith) { for (var ii = 0; ii < token.replaceWith.length; ii++) { result.push(token.replaceWith[ii]); } } if (!token.isMarkedForDeletion) { result.push(token); } token.isMarkedForDeletion = false; token.replaceWith = null; } return result; }; return { compactShorthands: compactShorthands }; })();