Splitting the new property optimizer into separate modules
authorTimur Kristóf <venemo@msn.com>
Fri, 28 Feb 2014 13:28:35 +0000 (14:28 +0100)
committerTimur Kristóf <venemo@msn.com>
Fri, 28 Feb 2014 13:28:35 +0000 (14:28 +0100)
lib/properties/compact.js [deleted file]
lib/properties/optimizer.js
lib/properties/override-compactor.js [new file with mode: 0644]
lib/properties/processable.js [new file with mode: 0644]
lib/properties/shorthand-compactor.js [new file with mode: 0644]
lib/properties/token.js [new file with mode: 0644]
lib/properties/validator.js [new file with mode: 0644]

diff --git a/lib/properties/compact.js b/lib/properties/compact.js
deleted file mode 100644 (file)
index 02f1809..0000000
+++ /dev/null
@@ -1,1290 +0,0 @@
-
-// The algorithm here is designed to optimize properties in a CSS selector block
-// and output the smallest possible equivalent code. It is capable of
-//
-// 1. Merging properties that can override each other
-// 2. Shorthanding properties when it makes sense
-//
-// Details are determined by `processable` - look at its comments to see how.
-// This design has many benefits:
-//
-// * Can break up shorthands to their granular values
-// * Deals with cases when a shorthand overrides more granular properties
-// * Leaves fallbacks intact but merges equally understandable values
-// * Removes default values from shorthand declarations
-// * Opens up opportunities for further optimalizations because granular components of shorthands are much easier to compare/process individually
-//
-
-module.exports = (function () {
-
-  // Creates a property token with its default value
-  var makeDefaultProperty = function (prop, important, newValue) {
-    return {
-      prop: prop,
-      value: newValue || processable[prop].defaultValue,
-      isImportant: important
-    };
-  };
-
-  // Creates an array of property tokens with their default values
-  var makeDefaultProperties = function (props, important) {
-    return props.map(function(prop) { return makeDefaultProperty(prop, important); });
-  };
-
-  // Regexes used for stuff
-  var cssUnitRegexStr =             '(\\-?\\.?\\d+\\.?\\d*(px|%|em|rem|in|cm|mm|ex|pt|pc|)|auto|inherit)';
-  var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)';
-  var cssFunctionVendorRegexStr =   '\\-(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)';
-  var cssFunctionAnyRegexStr =      '(' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
-  var cssUnitAnyRegexStr =          '(' + cssUnitRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
-
-  // Validator
-  // NOTE: The point here is not semantical but syntactical validity
-  var validator = {
-    isValidHexColor: function (s) {
-      return (s.length === 4 || s.length === 7) && s[0] === '#';
-    },
-    isValidRgbaColor: function (s) {
-      s = s.split(' ').join('');
-      return s.length > 0 && s.indexOf('rgba(') === 0 && s.indexOf(')') === s.length - 1;
-    },
-    isValidHslaColor: function (s) {
-      s = s.split(' ').join('');
-      return s.length > 0 && s.indexOf('hsla(') === 0 && s.indexOf(')') === s.length - 1;
-    },
-    isValidNamedColor: function (s) {
-      // TODO: we don't really check if it's a valid color value, but allow any letters in it
-      return s !== 'auto' && (s === 'transparent' || s === 'inherit' || /^[a-zA-Z]+$/.test(s));
-    },
-    isValidColor: function (s) {
-      // http://www.w3schools.com/cssref/css_colors_legal.asp
-      return validator.isValidNamedColor(s) || validator.isValidHexColor(s) || validator.isValidRgbaColor(s) || validator.isValidHslaColor(s);
-    },
-    isValidUrl: function (s) {
-      // NOTE: at this point all URLs are replaced with placeholders by clean-css, so we check for those placeholders
-      return s.indexOf('__ESCAPED_URL_CLEAN_CSS') === 0;
-    },
-    isValidUnit: function (s) {
-      return new RegExp('^' + cssUnitAnyRegexStr + '$', 'gi').test(s);
-    },
-    isValidUnitWithoutFunction: function (s) {
-      return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s);
-    },
-    isValidFunctionWithoutVendorPrefix: function (s) {
-      return new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'gi').test(s);
-    },
-    isValidFunctionWithVendorPrefix: function (s) {
-      return new RegExp('^' + cssFunctionVendorRegexStr + '$', 'gi').test(s);
-    },
-    isValidFunction: function (s) {
-      return new RegExp('^' + cssFunctionAnyRegexStr + '$', 'gi').test(s);
-    },
-    isValidBackgroundRepeat: function (s) {
-      return s === 'repeat' || s === 'no-repeat' || s === 'repeat-x' || s === 'repeat-y' || s === 'inherit';
-    },
-    isValidBackgroundAttachment: function (s) {
-      return s === 'inherit' || s === 'scroll' || s === 'fixed' || s === 'local';
-    },
-    isValidBackgroundPositionPart: function (s) {
-      if (s === 'center' || s === 'top' || s === 'bottom' || s === 'left' || s === 'right')
-        return true;
-      // LIMITATION: currently we don't support functions in here because otherwise we'd confuse things like linear-gradient()
-      //             we need to figure out the complete list of functions that are allowed for units and then we can use isValidUnit here.
-      return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s);
-    },
-    isValidBackgroundPosition: function (s) {
-      if (s === 'inherit')
-        return true;
-      return s.split(' ').every(function(p) { return validator.isValidBackgroundPositionPart(p); });
-    },
-    isValidListStyleType: function (s) {
-      return s === 'armenian' || s === 'circle' || s === 'cjk-ideographic' || s === 'decimal' || s === 'decimal-leading-zero' || s === 'disc' || s === 'georgian' || s === 'hebrew' || s === 'hiragana' || s === 'hiragana-iroha' || s === 'inherit' || s === 'katakana' || s === 'katakana-iroha' || s === 'lower-alpha' || s === 'lower-greek' || s === 'lower-latin' || s === 'lower-roman' || s === 'none' || s === 'square' || s === 'upper-alpha' || s === 'upper-latin' || s === 'upper-roman';
-    },
-    isValidListStylePosition: function (s) {
-      return s === 'inside' || s === 'outside' || s === 'inherit';
-    },
-    isValidOutlineColor: function (s) {
-      return s === 'invert' || validator.isValidColor(s) || validator.isValidVendorPrefixedValue(s);
-    },
-    isValidOutlineStyle: function (s) {
-      return s === 'inherit' || s === 'hidden' || s === 'none' || s === 'dotted' || s === 'dashed' || s === 'solid' || s === 'double' || s === 'groove' || s === 'ridge' || s === 'inset' || s === 'outset';
-    },
-    isValidOutlineWidth: function (s) {
-      return validator.isValidUnit(s) || s === 'thin' || s === 'thick' || s === 'medium' || s === 'inherit';
-    },
-    isValidVendorPrefixedValue: function (s) {
-      return /^-([A-Za-z0-9]|-)*$/gi.test(s);
-    },
-    areSameFunction: function (a, b) {
-      if (!validator.isValidFunction(a) || !validator.isValidFunction(b)) {
-        return false;
-      }
-      var f1name = a.substring(0, a.indexOf('('));
-      var f2name = b.substring(0, b.indexOf('('));
-
-      return f1name === f2name;
-    }
-  };
-  validator = Object.freeze(validator);
-
-  // Merge functions
-  var canMerge = {
-    // Use when two tokens of the same property can always be merged
-    always: function () {
-      // NOTE: We could have (t1, t2) parameters here but jshint complains because we don't use them
-      return true;
-    },
-    // Use when two tokens of the same property can only be merged if they have the same value
-    sameValue: function (t1, t2) {
-
-      return t1.value === t2.value;
-    },
-    sameFunctionOrValue: function (t1, t2) {
-      // Functions with the same name can override each other
-      if (validator.areSameFunction(t1.value, t2.value)) {
-        return true;
-      }
-
-      return t1.value === t2.value;
-    },
-    // Use for properties containing CSS units (margin-top, padding-left, etc.)
-    unit: function (t1, t2) {
-      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
-      // Understandability: (unit without functions) > (same functions | standard functions) > anything else
-      // NOTE: there is no point in having different vendor-specific functions override each other or standard functions,
-      //       or having standard functions override vendor-specific functions, but standard functions can override each other
-      // NOTE: vendor-specific property values are not taken into consideration here at the moment
-
-      if (validator.isValidUnitWithoutFunction(t2.value))
-        return true;
-      if (validator.isValidUnitWithoutFunction(t1.value))
-        return false;
-
-      // Standard non-vendor-prefixed functions can override each other
-      if (validator.isValidFunctionWithoutVendorPrefix(t2.value) && validator.isValidFunctionWithoutVendorPrefix(t1.value)) {
-        return true;
-      }
-
-      // Functions with the same name can override each other; same values can override each other
-      return canMerge.sameFunctionOrValue(t1, t2);
-    },
-    // Use for color properties (color, background-color, border-color, etc.)
-    color: function (t1, t2) {
-      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
-      // Understandability: (hex | named) > (rgba | hsla) > (same function name) > anything else
-      // NOTE: at this point rgb and hsl are replaced by hex values by clean-css
-
-      // (hex | named)
-      if (validator.isValidNamedColor(t2.value) || validator.isValidHexColor(t2.value))
-        return true;
-      if (validator.isValidNamedColor(t1.value) || validator.isValidHexColor(t1.value))
-        return false;
-
-      // (rgba|hsla)
-      if (validator.isValidRgbaColor(t2.value) || validator.isValidHslColor(t2.value) || validator.isValidHslaColor(t2.value))
-        return true;
-      if (validator.isValidRgbaColor(t1.value) || validator.isValidHslColor(t1.value) || validator.isValidHslaColor(t1.value))
-        return false;
-
-      // Functions with the same name can override each other; same values can override each other
-      return canMerge.sameFunctionOrValue(t1, t2);
-    },
-    // Use for background-image
-    backgroundImage: function (t1, t2) {
-      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
-      // Understandability: (none | url | inherit) > (same function) > (same value)
-
-      // (none | url)
-      if (t2.value === 'none' || t2.value === 'inherit' || validator.isValidUrl(t2.value))
-        return true;
-      if (t1.value === 'none' || t1.value === 'inherit' || validator.isValidUrl(t1.value))
-        return false;
-
-      // Functions with the same name can override each other; same values can override each other
-      return canMerge.sameFunctionOrValue(t1, t2);
-    }
-    // TODO: add more
-  };
-  canMerge = Object.freeze(canMerge);
-
-  // Functions for breaking up shorthands to components
-  var breakUp = {
-    // Use this for properties with 4 unit values (like margin or padding)
-    // NOTE: it handles shorter forms of these properties too (like, only 1, 2, or 3 units)
-    fourUnits: function (token) {
-      var descriptor = processable[token.prop];
-      var result = [];
-      var splitval = token.value.match(new RegExp(cssUnitAnyRegexStr, 'gi'));
-
-      if (splitval.length === 0 || (splitval.length < descriptor.components.length && descriptor.components.length > 4)) {
-        // This token is malformed and we have no idea how to fix it. So let's just keep it intact
-        return [token];
-      }
-
-      // Fix those that we do know how to fix
-      if (splitval.length < descriptor.components.length && splitval.length < 2) {
-        // foo{margin:1px} -> foo{margin:1px 1px}
-        splitval[1] = splitval[0];
-      }
-      if (splitval.length < descriptor.components.length && splitval.length < 3) {
-        // foo{margin:1px 2px} -> foo{margin:1px 2px 1px}
-        splitval[2] = splitval[0];
-      }
-      if (splitval.length < descriptor.components.length && splitval.length < 4) {
-        // foo{margin:1px 2px 3px} -> foo{margin:1px 2px 3px 2px}
-        splitval[3] = splitval[1];
-      }
-
-      // Now break it up to its components
-      for (var i = 0; i < descriptor.components.length; i++) {
-        var t = {
-          prop: descriptor.components[i],
-          value: splitval[i],
-          isImportant: token.isImportant
-        };
-        result.push(t);
-      }
-
-      return result;
-    },
-    // Breaks up a background property value
-    background: function (token) {
-      // Default values
-      var result = makeDefaultProperties(['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment'], token.isImportant);
-      var color = result[0], image = result[1], repeat = result[2], position = result[3], attachment = result[4];
-
-      // Take care of inherit
-      if (token.value === 'inherit') {
-        // NOTE: 'inherit' is not a valid value for background-attachment so there we'll leave the default value
-        color.value = image.value =  repeat.value = position.value = attachment.value = 'inherit';
-        return result;
-      }
-
-      // Break the background up into parts
-      var parts = token.value.split(' ');
-      if (parts.length === 0) {
-        return result;
-      }
-
-      // The trick here is that we start going through the parts from the end, then stop after background repeat,
-      // then start from the from the beginning until we find a valid color value. What remains will be treated as background-image.
-
-      var currentIndex = parts.length - 1;
-      var current = parts[currentIndex];
-      // Attachment
-      if (validator.isValidBackgroundAttachment(current)) {
-        // Found attachment
-        attachment.value = current;
-        currentIndex--;
-        current = parts[currentIndex];
-      }
-      // Position
-      var pos = parts[currentIndex - 1] + ' ' + parts[currentIndex];
-      if (currentIndex >= 1 && validator.isValidBackgroundPosition(pos)) {
-        // Found position (containing two parts)
-        position.value = pos;
-        currentIndex -= 2;
-        current = parts[currentIndex];
-      }
-      else if (currentIndex >= 0 && validator.isValidBackgroundPosition(current)) {
-        // Found position (containing just one part)
-        position.value = current;
-        currentIndex--;
-        current = parts[currentIndex];
-      }
-      // Repeat
-      if (currentIndex >= 0 && validator.isValidBackgroundRepeat(current)) {
-        // Found repeat
-        repeat.value = current;
-        currentIndex--;
-        current = parts[currentIndex];
-      }
-      // Color
-      var fromBeginning = 0;
-      if (validator.isValidColor(parts[0])) {
-        // Found color
-        color.value = parts[0];
-        fromBeginning = 1;
-      }
-      // Image
-      image.value = (parts.splice(fromBeginning, currentIndex - fromBeginning + 1).join(' ')) || 'none';
-
-      return result;
-    },
-    // Breaks up a list-style property value
-    listStyle: function (token) {
-      // Default values
-      var result = makeDefaultProperties(['list-style-type', 'list-style-position', 'list-style-image'], token.isImportant);
-      var type = result[0], position = result[1], image = result[2];
-
-      if (token.value === 'inherit') {
-        type.value = position.value = image.value = 'inherit';
-        return result;
-      }
-
-      var parts = token.value.split(' ');
-      var ci = 0;
-
-      // Type
-      if (ci < parts.length && validator.isValidListStyleType(parts[ci])) {
-        type.value = parts[ci];
-        ci++;
-      }
-      // Position
-      if (ci < parts.length && validator.isValidListStylePosition(parts[ci])) {
-        position.value = parts[ci];
-        ci++;
-      }
-      // Image
-      if (ci < parts.length) {
-        image.value = parts.splice(ci, parts.length - ci + 1).join(' ');
-      }
-
-      return result;
-    },
-    // Breaks up outline
-    outline: function (token) {
-      // Default values
-      var result = makeDefaultProperties(['outline-color', 'outline-style', 'outline-width'], token.isImportant);
-      var color = result[0], style = result[1], width = result[2];
-
-      // Take care of inherit
-      if (token.value === 'inherit' || token.value === 'inherit inherit inherit') {
-        color.value = style.value = width.value = 'inherit';
-        return result;
-      }
-
-      // NOTE: usually users don't follow the required order of parts in this shorthand,
-      // so we'll try to parse it caring as little about order as possible
-
-      var parts = token.value.split(' '), w;
-
-      if (parts.length === 0) {
-        return result;
-      }
-
-      if (parts.length >= 1) {
-        // Try to find outline-width, excluding inherit because that can be anything
-        w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineWidth(p); });
-        if (w.length) {
-          width.value = w[0];
-          parts.splice(parts.indexOf(w[0]), 1);
-        }
-      }
-      if (parts.length >= 1) {
-        // Try to find outline-style, excluding inherit because that can be anything
-        w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineStyle(p); });
-        if (w.length) {
-          style.value = w[0];
-          parts.splice(parts.indexOf(w[0]), 1);
-        }
-      }
-      if (parts.length >= 1) {
-        // Find outline-color but this time can catch inherit
-        w = parts.filter(function(p) { return validator.isValidOutlineColor(p); });
-        if (w.length) {
-          color.value = w[0];
-          parts.splice(parts.indexOf(w[0]), 1);
-        }
-      }
-
-      return result;
-    }
-  };
-  breakUp = Object.freeze(breakUp);
-
-  // Contains functions that can put together shorthands from their components
-  // NOTE: correct order of tokens is assumed inside these functions!
-  var putTogether = {
-    // Use this for properties which have four unit values (margin, padding, etc.)
-    // NOTE: optimizes to shorter forms too (that only specify 1, 2, or 3 values)
-    fourUnits: function (prop, tokens, isImportant) {
-      // See about irrelevant tokens
-      // NOTE: This will enable some crazy optimalizations for us.
-      if (tokens[0].isIrrelevant) {
-        tokens[0].value = tokens[2].value;
-      }
-      if (tokens[2].isIrrelevant) {
-        tokens[2].value = tokens[0].value;
-      }
-      if (tokens[1].isIrrelevant) {
-        tokens[1].value = tokens[3].value;
-      }
-      if (tokens[3].isIrrelevant) {
-        tokens[3].value = tokens[1].value;
-      }
-      if (tokens[0].isIrrelevant && tokens[2].isIrrelevant) {
-        if (tokens[1].value === tokens[3].value) {
-          tokens[0].value = tokens[2].value = tokens[1].value;
-        }
-        else {
-          tokens[0].value = tokens[2].value = '0';
-        }
-      }
-      if (tokens[1].isIrrelevant && tokens[3].isIrrelevant) {
-        if (tokens[0].value === tokens[2].value) {
-          tokens[1].value = tokens[3].value = tokens[0].value;
-        }
-        else {
-          tokens[1].value = tokens[3].value = '0';
-        }
-      }
-
-      var result = {
-        prop: prop,
-        value: tokens[0].value,
-        isImportant: isImportant,
-        granularValues: { }
-      };
-      result.granularValues[tokens[0].prop] = tokens[0].value;
-      result.granularValues[tokens[1].prop] = tokens[1].value;
-      result.granularValues[tokens[2].prop] = tokens[2].value;
-      result.granularValues[tokens[3].prop] = tokens[3].value;
-
-      // If all of them are irrelevant
-      if (tokens[0].isIrrelevant && tokens[1].isIrrelevant && tokens[2].isIrrelevant && tokens[3].isIrrelevant) {
-        result.value = processable[prop].shortestValue || processable[prop].defaultValue;
-        return result;
-      }
-
-      // 1-value short form: all four components are equal
-      if (tokens[0].value === tokens[1].value && tokens[0].value === tokens[2].value && tokens[0].value === tokens[3].value) {
-        return result;
-      }
-      result.value += ' ' + tokens[1].value;
-      // 2-value short form: first and third; second and fourth values are equal
-      if (tokens[0].value === tokens[2].value && tokens[1].value === tokens[3].value) {
-        return result;
-      }
-      result.value += ' ' + tokens[2].value;
-      // 3-value short form: second and fourth values are equal
-      if (tokens[1].value === tokens[3].value) {
-        return result;
-      }
-      // 4-value form (none of the above optimalizations could be accomplished)
-      result.value += ' ' + tokens[3].value;
-      return result;
-    },
-    // Puts together the components by spaces and omits default values (this is the case for most shorthands)
-    bySpacesOmitDefaults: function (prop, tokens, isImportant) {
-      var result = {
-        prop: prop,
-        value: '',
-        isImportant: isImportant
-      };
-      // Get irrelevant tokens
-      var irrelevantTokens = tokens.filter(function (t) { return t.isIrrelevant; });
-
-      // If every token is irrelevant, return shortest possible value, fallback to default value
-      if (irrelevantTokens.length === tokens.length) {
-        result.isIrrelevant = true;
-        result.value = processable[prop].shortestValue || processable[prop].defaultValue;
-        return result;
-      }
-
-      // This will be the value of the shorthand if all the components are default
-      var valueIfAllDefault = processable[prop].defaultValue;
-
-      // Go through all tokens and concatenate their values as necessary
-      for (var i = 0; i < tokens.length; i++) {
-        var token = tokens[i];
-
-        // Set granular value so that other parts of the code can use this for optimalization opportunities
-        result.granularValues = result.granularValues || { };
-        result.granularValues[token.prop] = token.value;
-
-        // Use irrelevant tokens for optimalization opportunity
-        if (token.isIrrelevant) {
-          // Get shortest possible value, fallback to default value
-          var tokenShortest = processable[token.prop].shortestValue || processable[token.prop].defaultValue;
-          // If the shortest possible value of this token is shorter than the default value of the shorthand, use it instead
-          if (tokenShortest.length < valueIfAllDefault.length) {
-            valueIfAllDefault = tokenShortest;
-          }
-        }
-
-        // Omit default / irrelevant value
-        if (token.isIrrelevant || (processable[token.prop] && processable[token.prop].defaultValue === token.value)) {
-          continue;
-        }
-
-        result.value += ' ' + token.value;
-      }
-
-      result.value = result.value.trim();
-      if (!result.value) {
-        result.value = valueIfAllDefault;
-      }
-
-      return result;
-    },
-    // Handles the cases when some or all the fine-grained properties are set to inherit
-    takeCareOfInherit: function (innerFunc) {
-      return function (prop, tokens, isImportant) {
-        // Filter out the inheriting and non-inheriting tokens in one iteration
-        var inheritingTokens = [];
-        var nonInheritingTokens = [];
-        var result2Shorthandable = [];
-        var i;
-        for (i = 0; i < tokens.length; i++) {
-          if (tokens[i].value === 'inherit') {
-            inheritingTokens.push(tokens[i]);
-            result2Shorthandable.push({
-              prop: tokens[i].prop,
-              value: processable[tokens[i].prop].defaultValue,
-              isImportant: tokens[i].isImportant,
-              // Indicate that this property is irrelevant and its value can safely be set to anything else
-              isIrrelevant: true
-            });
-          }
-          else {
-            nonInheritingTokens.push(tokens[i]);
-            result2Shorthandable.push(tokens[i]);
-          }
-        }
-
-        // When all the tokens are 'inherit'
-        if (nonInheritingTokens.length === 0) {
-          return {
-            prop: prop,
-            value: 'inherit',
-            isImportant: isImportant
-          };
-        }
-        // When some (but not all) of the tokens are 'inherit'
-        else if (inheritingTokens.length > 0) {
-          // Result 1. Shorthand just the inherit values and have it overridden with the non-inheriting ones
-          var result1 = [{
-            prop: prop,
-            value: 'inherit',
-            isImportant: isImportant
-          }].concat(nonInheritingTokens);
-
-          // Result 2. Shorthand every non-inherit value and then have it overridden with the inheriting ones
-          var result2 = [innerFunc(prop, result2Shorthandable, isImportant)].concat(inheritingTokens);
-
-          // Return whichever is shorter
-          var dl1 = getDetokenizedLength(result1);
-          var dl2 = getDetokenizedLength(result2);
-
-          return dl1 < dl2 ? result1 : result2;
-        }
-        // When none of tokens are 'inherit'
-        else {
-          return innerFunc(prop, tokens, isImportant);
-        }
-      };
-    }
-  };
-  putTogether = Object.freeze(putTogether);
-
-  // Properties to process
-  // Extend this object in order to add support for more properties in the optimizer.
-  //
-  // Each key in this object represents a CSS property and should be an object.
-  // Such an object contains properties that describe how the represented CSS property should be handled.
-  // Possible options:
-  //
-  // * components: array (Only specify for shorthand properties.)
-  //   Contains the names of the granular properties this shorthand compacts.
-  //
-  // * canMerge: function (Default is canMerge.sameValue - meaning that they'll only be merged if they have the same value.)
-  //   Returns whether two tokens of this property can be merged with each other.
-  //   This property has no meaning for shorthands.
-  //
-  // * defaultValue: string
-  //   Specifies the default value of the property according to the CSS standard.
-  //   For shorthand, this is used when every component is set to its default value, therefore it should be the shortest possible default value of all the components.
-  //
-  // * shortestValue: string
-  //   Specifies the shortest possible value the property can possibly have.
-  //   (Falls back to defaultValue if unspecified.)
-  //
-  // * breakUp: function (Only specify for shorthand properties.)
-  //   Breaks the shorthand up to its components.
-  //
-  // * putTogether: function (Only specify for shorthand properties.)
-  //   Puts the shorthand together from its components.
-  //
-  var processable = {
-    'margin': {
-      components: [
-        'margin-top',
-        'margin-right',
-        'margin-bottom',
-        'margin-left'
-      ],
-      breakUp: breakUp.fourUnits,
-      putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits),
-      defaultValue: '0'
-    },
-    'margin-top': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'margin-right': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'margin-bottom': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'margin-left': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'padding': {
-      components: [
-        'padding-top',
-        'padding-right',
-        'padding-bottom',
-        'padding-left'
-      ],
-      breakUp: breakUp.fourUnits,
-      putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits),
-      defaultValue: '0'
-    },
-    'padding-top': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'padding-right': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'padding-bottom': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'padding-left': {
-      defaultValue: '0',
-      canMerge: canMerge.unit
-    },
-    'background': {
-      components: [
-        'background-color',
-        'background-image',
-        'background-repeat',
-        'background-position',
-        'background-attachment'
-      ],
-      breakUp: breakUp.background,
-      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
-      defaultValue: '0 0',
-      shortestValue: '0'
-    },
-    'color': {
-      canMerge: canMerge.color,
-      defaultValue: 'transparent',
-      shortestValue: 'red'
-    },
-    'background-color': {
-      // http://www.w3schools.com/cssref/pr_background-color.asp
-      canMerge: canMerge.color,
-      defaultValue: 'transparent',
-      shortestValue: 'red'
-    },
-    'background-image': {
-      // http://www.w3schools.com/cssref/pr_background-image.asp
-      canMerge: canMerge.backgroundImage,
-      defaultValue: 'none'
-    },
-    'background-repeat': {
-      // http://www.w3schools.com/cssref/pr_background-repeat.asp
-      canMerge: canMerge.always,
-      defaultValue: 'repeat'
-    },
-    'background-position': {
-      // http://www.w3schools.com/cssref/pr_background-position.asp
-      canMerge: canMerge.always,
-      defaultValue: '0 0',
-      shortestValue: '0'
-    },
-    'background-attachment': {
-      // http://www.w3schools.com/cssref/pr_background-attachment.asp
-      canMerge: canMerge.always,
-      defaultValue: 'scroll'
-    },
-    'list-style': {
-      // http://www.w3schools.com/cssref/pr_list-style.asp
-      components: [
-        'list-style-type',
-        'list-style-position',
-        'list-style-image'
-      ],
-      canMerge: canMerge.always,
-      breakUp: breakUp.listStyle,
-      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
-      defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for <ol>
-      shortestValue: 'none'
-    },
-    'list-style-type' : {
-      // http://www.w3schools.com/cssref/pr_list-style-type.asp
-      canMerge: canMerge.always,
-      shortestValue: 'none',
-      defaultValue: '__hack'
-      // NOTE: we can't tell the real default value here, it's 'disc' for <ul> and 'decimal' for <ol>
-      //       -- this is a hack, but it doesn't matter because this value will be either overridden or it will disappear at the final step anyway
-    },
-    'list-style-position' : {
-      // http://www.w3schools.com/cssref/pr_list-style-position.asp
-      canMerge: canMerge.always,
-      defaultValue: 'outside',
-      shortestValue: 'inside'
-    },
-    'list-style-image' : {
-      // http://www.w3schools.com/cssref/pr_list-style-image.asp
-      canMerge: canMerge.always,
-      defaultValue: 'none'
-    },
-    'outline': {
-      components: [
-        'outline-color',
-        'outline-style',
-        'outline-width'
-      ],
-      breakUp: breakUp.outline,
-      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
-      defaultValue: '0'
-    },
-    'outline-color': {
-      // http://www.w3schools.com/cssref/pr_outline-color.asp
-      canMerge: canMerge.color,
-      defaultValue: 'invert',
-      shortestValue: 'red'
-    },
-    'outline-style': {
-      // http://www.w3schools.com/cssref/pr_outline-style.asp
-      canMerge: canMerge.always,
-      defaultValue: 'none'
-    },
-    'outline-width': {
-      // http://www.w3schools.com/cssref/pr_outline-width.asp
-      canMerge: canMerge.unit,
-      defaultValue: 'medium',
-      shortestValue: '0'
-    }
-    // TODO: add more
-  };
-  for (var proc in processable) {
-    if (processable.hasOwnProperty(proc)) {
-      var currDesc = processable[proc];
-
-      if (currDesc.components instanceof Array && currDesc.components.length) {
-        currDesc.isShorthand = true;
-
-        for (var cI = 0; cI < currDesc.components.length; cI++) {
-          if (!processable[currDesc.components[cI]]) {
-            throw new Error('"' + currDesc.components[cI] + '" is defined as a component of "' + proc + '" but isn\'t defined in processable.');
-          }
-          processable[currDesc.components[cI]].componentOf = proc;
-        }
-      }
-
-      currDesc.defaultToken = makeDefaultProperty(proc);
-    }
-  }
-  processable = Object.freeze(processable);
-
-  var isHackValue = function (t) { return t.value === '__hack'; };
-  var important = '!important';
-
-  // Tells if the first parameter is a component of the second one
-  var isComponentOf = function (t1, t2) {
-    if (!processable[t1.prop] || !processable[t2.prop])
-      return false;
-    if (!(processable[t2.prop].components instanceof Array) || !processable[t2.prop].components.length)
-      return false;
-
-    return processable[t2.prop].components.indexOf(t1.prop) >= 0;
-  };
-
-  // Breaks up the CSS properties so that they can be handled more easily
-  var tokenize = function (input) {
-    // Split the input by semicolons and parse the parts
-    var tokens = input.split(';').map(function(fullProp) {
-      // Find first colon
-      var colonPos = fullProp.indexOf(':');
-
-      if (colonPos < 0) {
-        // This property doesn't have a colon, it's invalid. Let's keep it intact anyway.
-        return {
-          value: fullProp
-        };
-      }
-
-      // Parse parts of the property
-      var prop = fullProp.substr(0, colonPos).trim();
-      var value = fullProp.substr(colonPos + 1).trim();
-      var isImportant = false;
-      var importantPos = value.indexOf(important);
-
-      // Check if the property is important
-      if (importantPos >= 1 && importantPos === value.length - important.length) {
-        value = value.substr(0, importantPos).trim();
-        isImportant = true;
-      }
-
-      // Return result
-      var result = {
-        prop: prop,
-        value: value,
-        isImportant: isImportant
-      };
-
-      // If this is a shorthand, break up its values
-      // NOTE: we need to do this for all shorthands because otherwise we couldn't remove default values from them
-      if (processable[prop] && processable[prop].isShorthand) {
-        result.isShorthand = true;
-        result.components = processable[prop].breakUp(result);
-        result.isDirty = true;
-      }
-
-      return result;
-    });
-
-    return tokens;
-  };
-
-  // Transforms tokens back into CSS properties
-  var detokenize = function (tokens) {
-    // If by mistake the input is not an array, make it an array
-    if (!(tokens instanceof Array)) {
-      tokens = [tokens];
-    }
-
-    // This step takes care of putting together the components of shorthands
-    // NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
-    for (var i = 0; i < tokens.length; i++) {
-      var t = tokens[i];
-      if (t.isShorthand && t.isDirty) {
-        var news = processable[t.prop].putTogether(t.prop, t.components, t.isImportant);
-        Array.prototype.splice.apply(tokens, [i, 1].concat(news));
-        t.isDirty = false;
-        i--;
-      }
-    }
-
-    // And now, simply map every token into its string representation and concat them with a semicolon
-    var str = tokens.map(function(token) {
-      var result = '';
-
-      // NOTE: malformed tokens will not have a 'prop' property
-      if (token.prop) {
-        result += token.prop + ':';
-      }
-      if (token.value) {
-        result += token.value;
-      }
-      if (token.isImportant) {
-        result += important;
-      }
-
-      return result;
-    }).join(';');
-
-    return str;
-  };
-
-  // Gets the final (detokenized) length of the given tokens
-  var getDetokenizedLength = function (tokens) {
-    // If by mistake the input is not an array, make it an array
-    if (!(tokens instanceof Array)) {
-      tokens = [tokens];
-    }
-
-    var result = 0;
-
-    // This step takes care of putting together the components of shorthands
-    // NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
-    for (var i = 0; i < tokens.length; i++) {
-      var t = tokens[i];
-      if (t.isShorthand && t.isDirty) {
-        var news = processable[t.prop].putTogether(t.prop, t.components, t.isImportant);
-        Array.prototype.splice.apply(tokens, [i, 1].concat(news));
-        t.isDirty = false;
-        i--;
-        continue;
-      }
-
-      if (t.prop) {
-        result += t.prop.length;
-      }
-      if (t.value) {
-        result += t.value.length;
-      }
-      if (t.isImportant) {
-        result += important.length;
-      }
-    }
-
-    return result;
-  };
-
-  // Merges same properties
-  // https://github.com/GoalSmashers/clean-css/issues/173
-  // https://github.com/GoalSmashers/clean-css/issues/168
-  var mergeOverrides = function (tokens) {
-    var result, can, token, t, i, ii, oldResult, matchingComponent;
-
-    // Filter function used for finding out if `token` can't override another token
-    var cantOverrideFilter = function (t) {
-      return !(t.prop === token.prop && can(t, token));
-    };
-    // Used when searching for a component that matches token
-    var nameMatchFilter1 = function (x) {
-      return x.prop === token.prop;
-    };
-    // Used when searching for a component that matches t
-    var nameMatchFilter2 = function (x) {
-      return x.prop === t.prop;
-    };
-
-    // Go from the end and always take what the current token can't override as the new result set
-    for (result = tokens, i = 0; (ii = result.length - 1 - i) >= 0; i++) {
-      token = result[ii];
-      //console.log(i, ii, token.prop);
-      can = (processable[token.prop] && processable[token.prop].canMerge) || canMerge.sameValue;
-      oldResult = result;
-      result = [];
-
-      // Special flag which indicates that the current token should be removed
-      var removeSelf = false;
-
-      for (var iii = 0; iii < oldResult.length; iii++) {
-        t = oldResult[iii];
-
-        // A token can't override itself (checked by reference, not by value)
-        // NOTE: except when we explicitly tell it to remove itself
-        if (t === token && !removeSelf) {
-          result.push(t);
-          continue;
-        }
-
-        // Only an important token can even try to override tokens that come after it
-        if (iii > ii && !token.isImportant) {
-          result.push(t);
-          continue;
-        }
-
-        // A nonimportant token can never override an important one
-        if (t.isImportant && !token.isImportant) {
-          result.push(t);
-          continue;
-        }
-
-        if (token.isShorthand && !t.isShorthand && isComponentOf(t, token)) {
-          // token (a shorthand) is trying to override t (a component)
-
-          // Find the matching component in the shorthand
-          matchingComponent = token.components.filter(nameMatchFilter2)[0];
-          can = (processable[t.prop] && processable[t.prop].canMerge) || canMerge.sameValue;
-          if (!can(t, matchingComponent)) {
-            // The shorthand can't override the component
-            result.push(t);
-          }
-        }
-        else if (t.isShorthand && !token.isShorthand && isComponentOf(token, t)) {
-          // token (a component) is trying to override a component of t (a shorthand)
-
-          // Find the matching component in the shorthand
-          matchingComponent = t.components.filter(nameMatchFilter1)[0];
-          if (can(matchingComponent, token)) {
-            // The component can override the matching component in the shorthand
-
-            if (!token.isImportant) {
-              // The overriding component is non-important which means we can simply include it into the shorthand
-              // NOTE: stuff that can't really be included, like inherit, is taken care of at the final step, not here
-              matchingComponent.value = token.value;
-              // We use the special flag to get rid of the component
-              removeSelf = true;
-            }
-            else {
-              // The overriding component is important; sadly we can't get rid of it,
-              // but we can still mark the matching component in the shorthand as irrelevant
-              matchingComponent.isIrrelevant = true;
-            }
-            t.isDirty = true;
-          }
-          result.push(t);
-        }
-        else if (token.isShorthand && t.isShorthand && token.prop === t.prop) {
-          // token is a shorthand and is trying to override another instance of the same shorthand
-
-          // Can only override other shorthand when each of its components can override each of the other's components
-          for (var iiii = 0; iiii < t.components.length; iiii++) {
-            can = (processable[t.components[iiii].prop] && processable[t.components[iiii].prop].canMerge) || canMerge.sameValue;
-            if (!can(t.components[iiii], token.components[iiii])) {
-              result.push(t);
-              break;
-            }
-          }
-        }
-        else if (cantOverrideFilter(t, iii)) {
-          // in every other case, use the override mechanism
-          result.push(t);
-        }
-      }
-      if (removeSelf) {
-        i--;
-      }
-    }
-
-    return result;
-  };
-
-  // Compacts the tokens by transforming properties into their shorthand notations when possible
-  // https://github.com/GoalSmashers/clean-css/issues/134
-  var compactToShorthands = function(tokens, isImportant) {
-    // 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]) {
-            for (var ii = 0; ii < componentsSoFar[shprop].found[prop].length; ii++) {
-              var comp = componentsSoFar[shprop].found[prop][ii];
-
-              if (!comp.isMarkedForDeletion) {
-                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;
-
-      // Check basics
-      if (!componentsSoFar[prop] || !componentsSoFar[prop].found)
-        return false;
-
-      // Find components for the shorthand
-      var components = [];
-      var realComponents = [];
-      for (i = 0; i < processable[prop].components.length; 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 = makeDefaultProperty(pp, isImportant, componentsSoFar[prop].lastShorthand.components[i].value);
-          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 === processable[prop].components.length) {
-        // 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].canMerge(processable[rc.prop].defaultToken, rc)) {
-            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 = getDetokenizedLength(compacted);
-      var authenticLength = getDetokenizedLength(realComponents);
-
-      // TODO: unit test for hacked value
-      if (realComponents.length === processable[prop].components.length || 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) {
-          // Try to compact what we've found so far
-          while (compactSoFar(token.prop)) { }
-          // Reset
-          initSoFar(token.prop, token, true);
-        }
-
-        // TODO: test case for shorthanding boundaries
-        // TODO: what happens if the importantness of the shorthand isn't the same as isImportant parameter?
-      }
-      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 = makeDefaultProperty(token.prop, isImportant, token.value);
-          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;
-  };
-
-  // Processes the input by calling the other functions
-  // input is the content of a selector block (excluding the braces), NOT a full selector
-  var process = function (input) {
-    var tokens = tokenize(input);
-
-    tokens = mergeOverrides(tokens);
-    tokens = compactToShorthands(tokens, false);
-    tokens = compactToShorthands(tokens, true);
-
-    return detokenize(tokens);
-  };
-
-  // Return the process function as module.exports
-  return process;
-
-})();
-
index 0e3ed5e..4fa83c2 100644 (file)
@@ -225,6 +225,7 @@ module.exports = function Optimizer(compatibility) {
   var rebuild = function(tokens) {
     var flat = [];
 
+
     for (var i = 0, l = tokens.length; i < l; i++) {
       flat.push(tokens[i][0] + ':' + tokens[i][1]);
     }
@@ -232,7 +233,19 @@ module.exports = function Optimizer(compatibility) {
     return flat.join(';');
   };
 
-  var compact = require('./compact');
+  var p = require('./processable');
+  var overrideCompactor = require('./override-compactor');
+  var shorthandCompactor = require('./shorthand-compactor');
+
+  var compact = function (input) {
+    var tokens = p.Token.tokenize(input);
+
+    tokens = overrideCompactor.compactOverrides(tokens, p.processable);
+    tokens = shorthandCompactor.compactShorthands(tokens, false, p.processable, p.Token);
+    tokens = shorthandCompactor.compactShorthands(tokens, true, p.processable, p.Token);
+
+    return p.Token.detokenize(tokens);
+  };
 
   return {
     process: function(body, allowAdjacent, skipCompacting) {
diff --git a/lib/properties/override-compactor.js b/lib/properties/override-compactor.js
new file mode 100644 (file)
index 0000000..b51f7b2
--- /dev/null
@@ -0,0 +1,116 @@
+
+// Compacts the given tokens according to their ability to override each other.
+
+module.exports = (function () {
+  var sameValue = function (val1, val2) { return val1 === val2; };
+
+  var compactOverrides = function (tokens, processable) {
+    var result, can, token, t, i, ii, oldResult, matchingComponent;
+
+    // Used when searching for a component that matches token
+    var nameMatchFilter1 = function (x) {
+      return x.prop === token.prop;
+    };
+    // Used when searching for a component that matches t
+    var nameMatchFilter2 = function (x) {
+      return x.prop === t.prop;
+    };
+
+    // Go from the end and always take what the current token can't override as the new result set
+    for (result = tokens, i = 0; (ii = result.length - 1 - i) >= 0; i++) {
+      token = result[ii];
+      //console.log(i, ii, token.prop);
+      can = (processable[token.prop] && processable[token.prop].canOverride) || sameValue;
+      oldResult = result;
+      result = [];
+
+      // Special flag which indicates that the current token should be removed
+      var removeSelf = false;
+
+      for (var iii = 0; iii < oldResult.length; iii++) {
+        t = oldResult[iii];
+
+        // A token can't override itself (checked by reference, not by value)
+        // NOTE: except when we explicitly tell it to remove itself
+        if (t === token && !removeSelf) {
+          result.push(t);
+          continue;
+        }
+
+        // Only an important token can even try to override tokens that come after it
+        if (iii > ii && !token.isImportant) {
+          result.push(t);
+          continue;
+        }
+
+        // A nonimportant token can never override an important one
+        if (t.isImportant && !token.isImportant) {
+          result.push(t);
+          continue;
+        }
+
+        if (token.isShorthand && !t.isShorthand && t.isComponentOf(token)) {
+          // token (a shorthand) is trying to override t (a component)
+
+          // Find the matching component in the shorthand
+          matchingComponent = token.components.filter(nameMatchFilter2)[0];
+          can = (processable[t.prop] && processable[t.prop].canOverride) || sameValue;
+          if (!can(t.value, matchingComponent.value)) {
+            // The shorthand can't override the component
+            result.push(t);
+          }
+        }
+        else if (t.isShorthand && !token.isShorthand && token.isComponentOf(t)) {
+          // token (a component) is trying to override a component of t (a shorthand)
+
+          // Find the matching component in the shorthand
+          matchingComponent = t.components.filter(nameMatchFilter1)[0];
+          if (can(matchingComponent.value, token.value)) {
+            // The component can override the matching component in the shorthand
+
+            if (!token.isImportant) {
+              // The overriding component is non-important which means we can simply include it into the shorthand
+              // NOTE: stuff that can't really be included, like inherit, is taken care of at the final step, not here
+              matchingComponent.value = token.value;
+              // We use the special flag to get rid of the component
+              removeSelf = true;
+            }
+            else {
+              // The overriding component is important; sadly we can't get rid of it,
+              // but we can still mark the matching component in the shorthand as irrelevant
+              matchingComponent.isIrrelevant = true;
+            }
+            t.isDirty = true;
+          }
+          result.push(t);
+        }
+        else if (token.isShorthand && t.isShorthand && token.prop === t.prop) {
+          // token is a shorthand and is trying to override another instance of the same shorthand
+
+          // Can only override other shorthand when each of its components can override each of the other's components
+          for (var iiii = 0; iiii < t.components.length; iiii++) {
+            can = (processable[t.components[iiii].prop] && processable[t.components[iiii].prop].canOverride) || sameValue;
+            if (!can(t.components[iiii].value, token.components[iiii].value)) {
+              result.push(t);
+              break;
+            }
+          }
+        }
+        else if (t.prop !== token.prop || !can(t.value, token.value)) {
+          // in every other case, use the override mechanism
+          result.push(t);
+        }
+      }
+      if (removeSelf) {
+        i--;
+      }
+    }
+
+    return result;
+  };
+
+  return {
+    compactOverrides: compactOverrides
+  };
+
+})();
diff --git a/lib/properties/processable.js b/lib/properties/processable.js
new file mode 100644 (file)
index 0000000..c339e00
--- /dev/null
@@ -0,0 +1,641 @@
+
+// Contains the interpretation of CSS properties, as used by the property optimizer
+
+module.exports = (function () {
+
+  var validator = require('./validator');
+
+  // Functions that decide what value can override what.
+  // The main purpose is to disallow removing CSS fallbacks.
+  // A separate implementation is needed for every different kind of CSS property.
+  // -----
+  // The generic idea is that properties that have wider browser support are 'more understandable'
+  // than others and that 'less understandable' values can't override more understandable ones.
+  var canOverride = {
+    // Use when two tokens of the same property can always be merged
+    always: function () {
+      // NOTE: We could have (val1, val2) parameters here but jshint complains because we don't use them
+      return true;
+    },
+    // Use when two tokens of the same property can only be merged if they have the same value
+    sameValue: function(val1, val2) {
+      return val1 === val2;
+    },
+    sameFunctionOrValue: function(val1, val2) {
+      // Functions with the same name can override each other
+      if (validator.areSameFunction(val1, val2)) {
+        return true;
+      }
+
+      return val1 === val2;
+    },
+    // Use for properties containing CSS units (margin-top, padding-left, etc.)
+    unit: function(val1, val2) {
+      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
+      // Understandability: (unit without functions) > (same functions | standard functions) > anything else
+      // NOTE: there is no point in having different vendor-specific functions override each other or standard functions,
+      //       or having standard functions override vendor-specific functions, but standard functions can override each other
+      // NOTE: vendor-specific property values are not taken into consideration here at the moment
+
+      if (validator.isValidUnitWithoutFunction(val2))
+        return true;
+      if (validator.isValidUnitWithoutFunction(val1))
+        return false;
+
+      // Standard non-vendor-prefixed functions can override each other
+      if (validator.isValidFunctionWithoutVendorPrefix(val2) && validator.isValidFunctionWithoutVendorPrefix(val1)) {
+        return true;
+      }
+
+      // Functions with the same name can override each other; same values can override each other
+      return canOverride.sameFunctionOrValue(val1, val2);
+    },
+    // Use for color properties (color, background-color, border-color, etc.)
+    color: function(val1, val2) {
+      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
+      // Understandability: (hex | named) > (rgba | hsla) > (same function name) > anything else
+      // NOTE: at this point rgb and hsl are replaced by hex values by clean-css
+
+      // (hex | named)
+      if (validator.isValidNamedColor(val2) || validator.isValidHexColor(val2))
+        return true;
+      if (validator.isValidNamedColor(val1) || validator.isValidHexColor(val1))
+        return false;
+
+      // (rgba|hsla)
+      if (validator.isValidRgbaColor(val2) || validator.isValidHslColor(val2) || validator.isValidHslaColor(val2))
+        return true;
+      if (validator.isValidRgbaColor(val1) || validator.isValidHslColor(val1) || validator.isValidHslaColor(val1))
+        return false;
+
+      // Functions with the same name can override each other; same values can override each other
+      return canOverride.sameFunctionOrValue(val1, val2);
+    },
+    // Use for background-image
+    backgroundImage: function(val1, val2) {
+      // The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
+      // Understandability: (none | url | inherit) > (same function) > (same value)
+
+      // (none | url)
+      if (val2 === 'none' || val2 === 'inherit' || validator.isValidUrl(val2))
+        return true;
+      if (val1 === 'none' || val1 === 'inherit' || validator.isValidUrl(val1))
+        return false;
+
+      // Functions with the same name can override each other; same values can override each other
+      return canOverride.sameFunctionOrValue(val1, val2);
+    }
+    // TODO: add more
+  };
+  canOverride = Object.freeze(canOverride);
+
+  // Functions for breaking up shorthands to components
+  var breakUp = {
+    // Use this for properties with 4 unit values (like margin or padding)
+    // NOTE: it handles shorter forms of these properties too (like, only 1, 2, or 3 units)
+    fourUnits: function (token) {
+      var descriptor = processable[token.prop];
+      var result = [];
+      var splitval = token.value.match(new RegExp(validator.cssUnitAnyRegexStr, 'gi'));
+
+      if (splitval.length === 0 || (splitval.length < descriptor.components.length && descriptor.components.length > 4)) {
+        // This token is malformed and we have no idea how to fix it. So let's just keep it intact
+        return [token];
+      }
+
+      // Fix those that we do know how to fix
+      if (splitval.length < descriptor.components.length && splitval.length < 2) {
+        // foo{margin:1px} -> foo{margin:1px 1px}
+        splitval[1] = splitval[0];
+      }
+      if (splitval.length < descriptor.components.length && splitval.length < 3) {
+        // foo{margin:1px 2px} -> foo{margin:1px 2px 1px}
+        splitval[2] = splitval[0];
+      }
+      if (splitval.length < descriptor.components.length && splitval.length < 4) {
+        // foo{margin:1px 2px 3px} -> foo{margin:1px 2px 3px 2px}
+        splitval[3] = splitval[1];
+      }
+
+      // Now break it up to its components
+      for (var i = 0; i < descriptor.components.length; i++) {
+        var t = new Token(descriptor.components[i], splitval[i], token.isImportant);
+        result.push(t);
+      }
+
+      return result;
+    },
+    // Breaks up a background property value
+    background: function (token) {
+      // Default values
+      var result = Token.makeDefaults(['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment'], token.isImportant);
+      var color = result[0], image = result[1], repeat = result[2], position = result[3], attachment = result[4];
+
+      // Take care of inherit
+      if (token.value === 'inherit') {
+        // NOTE: 'inherit' is not a valid value for background-attachment so there we'll leave the default value
+        color.value = image.value =  repeat.value = position.value = attachment.value = 'inherit';
+        return result;
+      }
+
+      // Break the background up into parts
+      var parts = token.value.split(' ');
+      if (parts.length === 0) {
+        return result;
+      }
+
+      // The trick here is that we start going through the parts from the end, then stop after background repeat,
+      // then start from the from the beginning until we find a valid color value. What remains will be treated as background-image.
+
+      var currentIndex = parts.length - 1;
+      var current = parts[currentIndex];
+      // Attachment
+      if (validator.isValidBackgroundAttachment(current)) {
+        // Found attachment
+        attachment.value = current;
+        currentIndex--;
+        current = parts[currentIndex];
+      }
+      // Position
+      var pos = parts[currentIndex - 1] + ' ' + parts[currentIndex];
+      if (currentIndex >= 1 && validator.isValidBackgroundPosition(pos)) {
+        // Found position (containing two parts)
+        position.value = pos;
+        currentIndex -= 2;
+        current = parts[currentIndex];
+      }
+      else if (currentIndex >= 0 && validator.isValidBackgroundPosition(current)) {
+        // Found position (containing just one part)
+        position.value = current;
+        currentIndex--;
+        current = parts[currentIndex];
+      }
+      // Repeat
+      if (currentIndex >= 0 && validator.isValidBackgroundRepeat(current)) {
+        // Found repeat
+        repeat.value = current;
+        currentIndex--;
+        current = parts[currentIndex];
+      }
+      // Color
+      var fromBeginning = 0;
+      if (validator.isValidColor(parts[0])) {
+        // Found color
+        color.value = parts[0];
+        fromBeginning = 1;
+      }
+      // Image
+      image.value = (parts.splice(fromBeginning, currentIndex - fromBeginning + 1).join(' ')) || 'none';
+
+      return result;
+    },
+    // Breaks up a list-style property value
+    listStyle: function (token) {
+      // Default values
+      var result = Token.makeDefaults(['list-style-type', 'list-style-position', 'list-style-image'], token.isImportant);
+      var type = result[0], position = result[1], image = result[2];
+
+      if (token.value === 'inherit') {
+        type.value = position.value = image.value = 'inherit';
+        return result;
+      }
+
+      var parts = token.value.split(' ');
+      var ci = 0;
+
+      // Type
+      if (ci < parts.length && validator.isValidListStyleType(parts[ci])) {
+        type.value = parts[ci];
+        ci++;
+      }
+      // Position
+      if (ci < parts.length && validator.isValidListStylePosition(parts[ci])) {
+        position.value = parts[ci];
+        ci++;
+      }
+      // Image
+      if (ci < parts.length) {
+        image.value = parts.splice(ci, parts.length - ci + 1).join(' ');
+      }
+
+      return result;
+    },
+    // Breaks up outline
+    outline: function (token) {
+      // Default values
+      var result = Token.makeDefaults(['outline-color', 'outline-style', 'outline-width'], token.isImportant);
+      var color = result[0], style = result[1], width = result[2];
+
+      // Take care of inherit
+      if (token.value === 'inherit' || token.value === 'inherit inherit inherit') {
+        color.value = style.value = width.value = 'inherit';
+        return result;
+      }
+
+      // NOTE: usually users don't follow the required order of parts in this shorthand,
+      // so we'll try to parse it caring as little about order as possible
+
+      var parts = token.value.split(' '), w;
+
+      if (parts.length === 0) {
+        return result;
+      }
+
+      if (parts.length >= 1) {
+        // Try to find outline-width, excluding inherit because that can be anything
+        w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineWidth(p); });
+        if (w.length) {
+          width.value = w[0];
+          parts.splice(parts.indexOf(w[0]), 1);
+        }
+      }
+      if (parts.length >= 1) {
+        // Try to find outline-style, excluding inherit because that can be anything
+        w = parts.filter(function(p) { return p !== 'inherit' && validator.isValidOutlineStyle(p); });
+        if (w.length) {
+          style.value = w[0];
+          parts.splice(parts.indexOf(w[0]), 1);
+        }
+      }
+      if (parts.length >= 1) {
+        // Find outline-color but this time can catch inherit
+        w = parts.filter(function(p) { return validator.isValidOutlineColor(p); });
+        if (w.length) {
+          color.value = w[0];
+          parts.splice(parts.indexOf(w[0]), 1);
+        }
+      }
+
+      return result;
+    }
+  };
+
+  // Contains functions that can put together shorthands from their components
+  // NOTE: correct order of tokens is assumed inside these functions!
+  var putTogether = {
+    // Use this for properties which have four unit values (margin, padding, etc.)
+    // NOTE: optimizes to shorter forms too (that only specify 1, 2, or 3 values)
+    fourUnits: function (prop, tokens, isImportant) {
+      // See about irrelevant tokens
+      // NOTE: This will enable some crazy optimalizations for us.
+      if (tokens[0].isIrrelevant) {
+        tokens[0].value = tokens[2].value;
+      }
+      if (tokens[2].isIrrelevant) {
+        tokens[2].value = tokens[0].value;
+      }
+      if (tokens[1].isIrrelevant) {
+        tokens[1].value = tokens[3].value;
+      }
+      if (tokens[3].isIrrelevant) {
+        tokens[3].value = tokens[1].value;
+      }
+      if (tokens[0].isIrrelevant && tokens[2].isIrrelevant) {
+        if (tokens[1].value === tokens[3].value) {
+          tokens[0].value = tokens[2].value = tokens[1].value;
+        }
+        else {
+          tokens[0].value = tokens[2].value = '0';
+        }
+      }
+      if (tokens[1].isIrrelevant && tokens[3].isIrrelevant) {
+        if (tokens[0].value === tokens[2].value) {
+          tokens[1].value = tokens[3].value = tokens[0].value;
+        }
+        else {
+          tokens[1].value = tokens[3].value = '0';
+        }
+      }
+
+      var result = new Token(prop, tokens[0].value, isImportant);
+      result.granularValues = [];
+      result.granularValues[tokens[0].prop] = tokens[0].value;
+      result.granularValues[tokens[1].prop] = tokens[1].value;
+      result.granularValues[tokens[2].prop] = tokens[2].value;
+      result.granularValues[tokens[3].prop] = tokens[3].value;
+
+      // If all of them are irrelevant
+      if (tokens[0].isIrrelevant && tokens[1].isIrrelevant && tokens[2].isIrrelevant && tokens[3].isIrrelevant) {
+        result.value = processable[prop].shortestValue || processable[prop].defaultValue;
+        return result;
+      }
+
+      // 1-value short form: all four components are equal
+      if (tokens[0].value === tokens[1].value && tokens[0].value === tokens[2].value && tokens[0].value === tokens[3].value) {
+        return result;
+      }
+      result.value += ' ' + tokens[1].value;
+      // 2-value short form: first and third; second and fourth values are equal
+      if (tokens[0].value === tokens[2].value && tokens[1].value === tokens[3].value) {
+        return result;
+      }
+      result.value += ' ' + tokens[2].value;
+      // 3-value short form: second and fourth values are equal
+      if (tokens[1].value === tokens[3].value) {
+        return result;
+      }
+      // 4-value form (none of the above optimalizations could be accomplished)
+      result.value += ' ' + tokens[3].value;
+      return result;
+    },
+    // Puts together the components by spaces and omits default values (this is the case for most shorthands)
+    bySpacesOmitDefaults: function (prop, tokens, isImportant) {
+      var result = new Token(prop, '', isImportant);
+      // Get irrelevant tokens
+      var irrelevantTokens = tokens.filter(function (t) { return t.isIrrelevant; });
+
+      // If every token is irrelevant, return shortest possible value, fallback to default value
+      if (irrelevantTokens.length === tokens.length) {
+        result.isIrrelevant = true;
+        result.value = processable[prop].shortestValue || processable[prop].defaultValue;
+        return result;
+      }
+
+      // This will be the value of the shorthand if all the components are default
+      var valueIfAllDefault = processable[prop].defaultValue;
+
+      // Go through all tokens and concatenate their values as necessary
+      for (var i = 0; i < tokens.length; i++) {
+        var token = tokens[i];
+
+        // Set granular value so that other parts of the code can use this for optimalization opportunities
+        result.granularValues = result.granularValues || { };
+        result.granularValues[token.prop] = token.value;
+
+        // Use irrelevant tokens for optimalization opportunity
+        if (token.isIrrelevant) {
+          // Get shortest possible value, fallback to default value
+          var tokenShortest = processable[token.prop].shortestValue || processable[token.prop].defaultValue;
+          // If the shortest possible value of this token is shorter than the default value of the shorthand, use it instead
+          if (tokenShortest.length < valueIfAllDefault.length) {
+            valueIfAllDefault = tokenShortest;
+          }
+        }
+
+        // Omit default / irrelevant value
+        if (token.isIrrelevant || (processable[token.prop] && processable[token.prop].defaultValue === token.value)) {
+          continue;
+        }
+
+        result.value += ' ' + token.value;
+      }
+
+      result.value = result.value.trim();
+      if (!result.value) {
+        result.value = valueIfAllDefault;
+      }
+
+      return result;
+    },
+    // Handles the cases when some or all the fine-grained properties are set to inherit
+    takeCareOfInherit: function (innerFunc) {
+      return function (prop, tokens, isImportant) {
+        // Filter out the inheriting and non-inheriting tokens in one iteration
+        var inheritingTokens = [];
+        var nonInheritingTokens = [];
+        var result2Shorthandable = [];
+        var i;
+        for (i = 0; i < tokens.length; i++) {
+          if (tokens[i].value === 'inherit') {
+            inheritingTokens.push(tokens[i]);
+
+            // Indicate that this property is irrelevant and its value can safely be set to anything else
+            var r2s = new Token(tokens[i].prop, tokens[i].isImportant);
+            r2s.isIrrelevant = true;
+            result2Shorthandable.push(r2s);
+          }
+          else {
+            nonInheritingTokens.push(tokens[i]);
+            result2Shorthandable.push(tokens[i]);
+          }
+        }
+
+        // When all the tokens are 'inherit'
+        if (nonInheritingTokens.length === 0) {
+          return new Token(prop, 'inherit', isImportant);
+        }
+        // When some (but not all) of the tokens are 'inherit'
+        else if (inheritingTokens.length > 0) {
+          // Result 1. Shorthand just the inherit values and have it overridden with the non-inheriting ones
+          var result1 = [new Token(prop, 'inherit', isImportant)].concat(nonInheritingTokens);
+
+          // Result 2. Shorthand every non-inherit value and then have it overridden with the inheriting ones
+          var result2 = [innerFunc(prop, result2Shorthandable, isImportant)].concat(inheritingTokens);
+
+          // Return whichever is shorter
+          var dl1 = Token.getDetokenizedLength(result1);
+          var dl2 = Token.getDetokenizedLength(result2);
+
+          return dl1 < dl2 ? result1 : result2;
+        }
+        // When none of tokens are 'inherit'
+        else {
+          return innerFunc(prop, tokens, isImportant);
+        }
+      };
+    }
+  };
+
+  // Properties to process
+  // Extend this object in order to add support for more properties in the optimizer.
+  //
+  // Each key in this object represents a CSS property and should be an object.
+  // Such an object contains properties that describe how the represented CSS property should be handled.
+  // Possible options:
+  //
+  // * components: array (Only specify for shorthand properties.)
+  //   Contains the names of the granular properties this shorthand compacts.
+  //
+  // * canOverride: function (Default is canOverride.sameValue - meaning that they'll only be merged if they have the same value.)
+  //   Returns whether two tokens of this property can be merged with each other.
+  //   This property has no meaning for shorthands.
+  //
+  // * defaultValue: string
+  //   Specifies the default value of the property according to the CSS standard.
+  //   For shorthand, this is used when every component is set to its default value, therefore it should be the shortest possible default value of all the components.
+  //
+  // * shortestValue: string
+  //   Specifies the shortest possible value the property can possibly have.
+  //   (Falls back to defaultValue if unspecified.)
+  //
+  // * breakUp: function (Only specify for shorthand properties.)
+  //   Breaks the shorthand up to its components.
+  //
+  // * putTogether: function (Only specify for shorthand properties.)
+  //   Puts the shorthand together from its components.
+  //
+  var processable = {
+    'margin': {
+      components: [
+        'margin-top',
+        'margin-right',
+        'margin-bottom',
+        'margin-left'
+      ],
+      breakUp: breakUp.fourUnits,
+      putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits),
+      defaultValue: '0'
+    },
+    'margin-top': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'margin-right': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'margin-bottom': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'margin-left': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'padding': {
+      components: [
+        'padding-top',
+        'padding-right',
+        'padding-bottom',
+        'padding-left'
+      ],
+      breakUp: breakUp.fourUnits,
+      putTogether: putTogether.takeCareOfInherit(putTogether.fourUnits),
+      defaultValue: '0'
+    },
+    'padding-top': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'padding-right': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'padding-bottom': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'padding-left': {
+      defaultValue: '0',
+      canOverride: canOverride.unit
+    },
+    'background': {
+      components: [
+        'background-color',
+        'background-image',
+        'background-repeat',
+        'background-position',
+        'background-attachment'
+      ],
+      breakUp: breakUp.background,
+      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
+      defaultValue: '0 0',
+      shortestValue: '0'
+    },
+    'color': {
+      canOverride: canOverride.color,
+      defaultValue: 'transparent',
+      shortestValue: 'red'
+    },
+    'background-color': {
+      canOverride: canOverride.color,
+      defaultValue: 'transparent',
+      shortestValue: 'red'
+    },
+    'background-image': {
+      canOverride: canOverride.backgroundImage,
+      defaultValue: 'none'
+    },
+    'background-repeat': {
+      canOverride: canOverride.always,
+      defaultValue: 'repeat'
+    },
+    'background-position': {
+      canOverride: canOverride.always,
+      defaultValue: '0 0',
+      shortestValue: '0'
+    },
+    'background-attachment': {
+      canOverride: canOverride.always,
+      defaultValue: 'scroll'
+    },
+    'list-style': {
+      components: [
+        'list-style-type',
+        'list-style-position',
+        'list-style-image'
+      ],
+      canOverride: canOverride.always,
+      breakUp: breakUp.listStyle,
+      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
+      defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for <ol>
+      shortestValue: 'none'
+    },
+    'list-style-type' : {
+      canOverride: canOverride.always,
+      shortestValue: 'none',
+      defaultValue: '__hack'
+      // NOTE: we can't tell the real default value here, it's 'disc' for <ul> and 'decimal' for <ol>
+      //       -- this is a hack, but it doesn't matter because this value will be either overridden or it will disappear at the final step anyway
+    },
+    'list-style-position' : {
+      canOverride: canOverride.always,
+      defaultValue: 'outside',
+      shortestValue: 'inside'
+    },
+    'list-style-image' : {
+      canOverride: canOverride.always,
+      defaultValue: 'none'
+    },
+    'outline': {
+      components: [
+        'outline-color',
+        'outline-style',
+        'outline-width'
+      ],
+      breakUp: breakUp.outline,
+      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults),
+      defaultValue: '0'
+    },
+    'outline-color': {
+      canOverride: canOverride.color,
+      defaultValue: 'invert',
+      shortestValue: 'red'
+    },
+    'outline-style': {
+      canOverride: canOverride.always,
+      defaultValue: 'none'
+    },
+    'outline-width': {
+      canOverride: canOverride.unit,
+      defaultValue: 'medium',
+      shortestValue: '0'
+    }
+    // TODO: add more
+  };
+
+  // Set some stuff iteratively
+  for (var proc in processable) {
+    if (processable.hasOwnProperty(proc)) {
+      var currDesc = processable[proc];
+
+      if (currDesc.components instanceof Array && currDesc.components.length) {
+        currDesc.isShorthand = true;
+
+        for (var cI = 0; cI < currDesc.components.length; cI++) {
+          if (!processable[currDesc.components[cI]]) {
+            throw new Error('"' + currDesc.components[cI] + '" is defined as a component of "' + proc + '" but isn\'t defined in processable.');
+          }
+          processable[currDesc.components[cI]].componentOf = proc;
+        }
+      }
+    }
+  }
+
+  var Token = require('./token').createTokenPrototype(processable);
+
+  return {
+    processable: processable,
+    Token: Token
+  };
+})();
diff --git a/lib/properties/shorthand-compactor.js b/lib/properties/shorthand-compactor.js
new file mode 100644 (file)
index 0000000..e116726
--- /dev/null
@@ -0,0 +1,246 @@
+
+// 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]) {
+            for (var ii = 0; ii < componentsSoFar[shprop].found[prop].length; ii++) {
+              var comp = componentsSoFar[shprop].found[prop][ii];
+
+              if (!comp.isMarkedForDeletion) {
+                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;
+
+      // Check basics
+      if (!componentsSoFar[prop] || !componentsSoFar[prop].found)
+        return false;
+
+      // Find components for the shorthand
+      var components = [];
+      var realComponents = [];
+      for (i = 0; i < processable[prop].components.length; 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 === processable[prop].components.length) {
+        // 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);
+
+      // TODO: unit test for hacked value
+      if (realComponents.length === processable[prop].components.length || 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) {
+          // Try to compact what we've found so far
+          while (compactSoFar(token.prop)) { }
+          // Reset
+          initSoFar(token.prop, token, true);
+        }
+
+        // TODO: test case for shorthanding boundaries
+        // TODO: what happens if the importantness of the shorthand isn't the same as isImportant parameter?
+      }
+      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
+  };
+
+})();
diff --git a/lib/properties/token.js b/lib/properties/token.js
new file mode 100644 (file)
index 0000000..bad10aa
--- /dev/null
@@ -0,0 +1,188 @@
+
+// Helper for tokenizing the contents of a CSS selector block
+
+module.exports = (function() {
+  var createTokenPrototype = function (processable) {
+    var important = '!important';
+
+    // Constructor for tokens
+    function Token (prop, p2, p3) {
+      this.prop = prop;
+      if (typeof(p2) === 'string') {
+        this.value = p2;
+        this.isImportant = p3;
+      }
+      else {
+        this.value = processable[prop].defaultValue;
+        this.isImportant = p2;
+      }
+    }
+
+    Token.prototype.prop = null;
+    Token.prototype.value = null;
+    Token.prototype.granularValues = null;
+    Token.prototype.components = null;
+    Token.prototype.position = null;
+    Token.prototype.isImportant = false;
+    Token.prototype.isDirty = false;
+    Token.prototype.isShorthand = false;
+    Token.prototype.isIrrelevant = false;
+    Token.prototype.isReal = true;
+    Token.prototype.isMarkedForDeletion = false;
+
+    // Tells if this token is a component of the other one
+    Token.prototype.isComponentOf = function (other) {
+      if (!processable[this.prop] || !processable[other.prop])
+        return false;
+      if (!(processable[other.prop].components instanceof Array) || !processable[other.prop].components.length)
+        return false;
+
+      return processable[other.prop].components.indexOf(this.prop) >= 0;
+    };
+
+    // Clones a token
+    Token.prototype.clone = function (isImportant) {
+      var token = new Token(this.prop, this.value, (typeof(isImportant) !== 'undefined' ? isImportant : this.isImportant));
+      return token;
+    };
+
+    // Creates an irrelevant token with the same prop
+    Token.prototype.cloneIrrelevant = function (isImportant) {
+      var token = Token.makeDefault(this.prop, (typeof(isImportant) !== 'undefined' ? isImportant : this.isImportant));
+      token.isIrrelevant = true;
+      return token;
+    };
+
+    // Creates an array of property tokens with their default values
+    Token.makeDefaults = function (props, important) {
+      return props.map(function(prop) {
+        return new Token(prop, important);
+      });
+    };
+
+    // Parses one CSS property declaration into a token
+    Token.tokenizeOne = function (fullProp) {
+      // Find first colon
+      var colonPos = fullProp.indexOf(':');
+
+      if (colonPos < 0) {
+        // This property doesn't have a colon, it's invalid. Let's keep it intact anyway.
+        return new Token('', fullProp);
+      }
+
+      // Parse parts of the property
+      var prop = fullProp.substr(0, colonPos).trim();
+      var value = fullProp.substr(colonPos + 1).trim();
+      var isImportant = false;
+      var importantPos = value.indexOf(important);
+
+      // Check if the property is important
+      if (importantPos >= 1 && importantPos === value.length - important.length) {
+        value = value.substr(0, importantPos).trim();
+        isImportant = true;
+      }
+
+      // Return result
+      var result = new Token(prop, value, isImportant);
+
+      // If this is a shorthand, break up its values
+      // NOTE: we need to do this for all shorthands because otherwise we couldn't remove default values from them
+      if (processable[prop] && processable[prop].isShorthand) {
+        result.isShorthand = true;
+        result.components = processable[prop].breakUp(result);
+        result.isDirty = true;
+      }
+
+      return result;
+    };
+
+    // Breaks up a string of CSS property declarations into tokens so that they can be handled more easily
+    Token.tokenize = function (input) {
+      // Split the input by semicolons and parse the parts
+      var tokens = input.split(';').map(Token.tokenizeOne);
+      return tokens;
+    };
+
+    // Transforms tokens back into CSS properties
+    Token.detokenize = function (tokens) {
+      // If by mistake the input is not an array, make it an array
+      if (!(tokens instanceof Array)) {
+        tokens = [tokens];
+      }
+
+      // This step takes care of putting together the components of shorthands
+      // NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
+      for (var i = 0; i < tokens.length; i++) {
+        var t = tokens[i];
+        if (t.isShorthand && t.isDirty) {
+          var news = processable[t.prop].putTogether(t.prop, t.components, t.isImportant);
+          Array.prototype.splice.apply(tokens, [i, 1].concat(news));
+          t.isDirty = false;
+          i--;
+        }
+      }
+
+      // And now, simply map every token into its string representation and concat them with a semicolon
+      var str = tokens.map(function(token) {
+        var result = '';
+
+        // NOTE: malformed tokens will not have a 'prop' property
+        if (token.prop) {
+          result += token.prop + ':';
+        }
+        if (token.value) {
+          result += token.value;
+        }
+        if (token.isImportant) {
+          result += important;
+        }
+
+        return result;
+      }).join(';');
+
+      return str;
+    };
+
+    // Gets the final (detokenized) length of the given tokens
+    Token.getDetokenizedLength = function (tokens) {
+      // If by mistake the input is not an array, make it an array
+      if (!(tokens instanceof Array)) {
+        tokens = [tokens];
+      }
+
+      var result = 0;
+
+      // This step takes care of putting together the components of shorthands
+      // NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
+      for (var i = 0; i < tokens.length; i++) {
+        var t = tokens[i];
+        if (t.isShorthand && t.isDirty) {
+          var news = processable[t.prop].putTogether(t.prop, t.components, t.isImportant);
+          Array.prototype.splice.apply(tokens, [i, 1].concat(news));
+          t.isDirty = false;
+          i--;
+          continue;
+        }
+
+        if (t.prop) {
+          result += t.prop.length + 1;
+        }
+        if (t.value) {
+          result += t.value.length;
+        }
+        if (t.isImportant) {
+          result += important.length;
+        }
+      }
+
+      return result;
+    };
+
+    return Token;
+  };
+
+  return {
+    createTokenPrototype: createTokenPrototype
+  };
+
+})();
diff --git a/lib/properties/validator.js b/lib/properties/validator.js
new file mode 100644 (file)
index 0000000..b9f4487
--- /dev/null
@@ -0,0 +1,105 @@
+
+// Validates various CSS property values
+
+module.exports = (function () {
+  // Regexes used for stuff
+  var cssUnitRegexStr =             '(\\-?\\.?\\d+\\.?\\d*(px|%|em|rem|in|cm|mm|ex|pt|pc|)|auto|inherit)';
+  var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)';
+  var cssFunctionVendorRegexStr =   '\\-(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.)*\\)';
+  var cssFunctionAnyRegexStr =      '(' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
+  var cssUnitAnyRegexStr =          '(' + cssUnitRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
+
+  var validator = {
+    isValidHexColor: function (s) {
+      return (s.length === 4 || s.length === 7) && s[0] === '#';
+    },
+    isValidRgbaColor: function (s) {
+      s = s.split(' ').join('');
+      return s.length > 0 && s.indexOf('rgba(') === 0 && s.indexOf(')') === s.length - 1;
+    },
+    isValidHslaColor: function (s) {
+      s = s.split(' ').join('');
+      return s.length > 0 && s.indexOf('hsla(') === 0 && s.indexOf(')') === s.length - 1;
+    },
+    isValidNamedColor: function (s) {
+      // TODO: we don't really check if it's a valid color value, but allow any letters in it
+      return s !== 'auto' && (s === 'transparent' || s === 'inherit' || /^[a-zA-Z]+$/.test(s));
+    },
+    isValidColor: function (s) {
+      // http://www.w3schools.com/cssref/css_colors_legal.asp
+      return validator.isValidNamedColor(s) || validator.isValidHexColor(s) || validator.isValidRgbaColor(s) || validator.isValidHslaColor(s);
+    },
+    isValidUrl: function (s) {
+      // NOTE: at this point all URLs are replaced with placeholders by clean-css, so we check for those placeholders
+      return s.indexOf('__ESCAPED_URL_CLEAN_CSS') === 0;
+    },
+    isValidUnit: function (s) {
+      return new RegExp('^' + cssUnitAnyRegexStr + '$', 'gi').test(s);
+    },
+    isValidUnitWithoutFunction: function (s) {
+      return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s);
+    },
+    isValidFunctionWithoutVendorPrefix: function (s) {
+      return new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'gi').test(s);
+    },
+    isValidFunctionWithVendorPrefix: function (s) {
+      return new RegExp('^' + cssFunctionVendorRegexStr + '$', 'gi').test(s);
+    },
+    isValidFunction: function (s) {
+      return new RegExp('^' + cssFunctionAnyRegexStr + '$', 'gi').test(s);
+    },
+    isValidBackgroundRepeat: function (s) {
+      return s === 'repeat' || s === 'no-repeat' || s === 'repeat-x' || s === 'repeat-y' || s === 'inherit';
+    },
+    isValidBackgroundAttachment: function (s) {
+      return s === 'inherit' || s === 'scroll' || s === 'fixed' || s === 'local';
+    },
+    isValidBackgroundPositionPart: function (s) {
+      if (s === 'center' || s === 'top' || s === 'bottom' || s === 'left' || s === 'right')
+        return true;
+      // LIMITATION: currently we don't support functions in here because otherwise we'd confuse things like linear-gradient()
+      //             we need to figure out the complete list of functions that are allowed for units and then we can use isValidUnit here.
+      return new RegExp('^' + cssUnitRegexStr + '$', 'gi').test(s);
+    },
+    isValidBackgroundPosition: function (s) {
+      if (s === 'inherit')
+        return true;
+      return s.split(' ').every(function(p) { return validator.isValidBackgroundPositionPart(p); });
+    },
+    isValidListStyleType: function (s) {
+      return s === 'armenian' || s === 'circle' || s === 'cjk-ideographic' || s === 'decimal' || s === 'decimal-leading-zero' || s === 'disc' || s === 'georgian' || s === 'hebrew' || s === 'hiragana' || s === 'hiragana-iroha' || s === 'inherit' || s === 'katakana' || s === 'katakana-iroha' || s === 'lower-alpha' || s === 'lower-greek' || s === 'lower-latin' || s === 'lower-roman' || s === 'none' || s === 'square' || s === 'upper-alpha' || s === 'upper-latin' || s === 'upper-roman';
+    },
+    isValidListStylePosition: function (s) {
+      return s === 'inside' || s === 'outside' || s === 'inherit';
+    },
+    isValidOutlineColor: function (s) {
+      return s === 'invert' || validator.isValidColor(s) || validator.isValidVendorPrefixedValue(s);
+    },
+    isValidOutlineStyle: function (s) {
+      return s === 'inherit' || s === 'hidden' || s === 'none' || s === 'dotted' || s === 'dashed' || s === 'solid' || s === 'double' || s === 'groove' || s === 'ridge' || s === 'inset' || s === 'outset';
+    },
+    isValidOutlineWidth: function (s) {
+      return validator.isValidUnit(s) || s === 'thin' || s === 'thick' || s === 'medium' || s === 'inherit';
+    },
+    isValidVendorPrefixedValue: function (s) {
+      return /^-([A-Za-z0-9]|-)*$/gi.test(s);
+    },
+    areSameFunction: function (a, b) {
+      if (!validator.isValidFunction(a) || !validator.isValidFunction(b)) {
+        return false;
+      }
+      var f1name = a.substring(0, a.indexOf('('));
+      var f2name = b.substring(0, b.indexOf('('));
+
+      return f1name === f2name;
+    }
+  };
+
+  validator.cssUnitRegexStr = cssUnitRegexStr;
+  validator.cssFunctionNoVendorRegexStr = cssFunctionNoVendorRegexStr;
+  validator.cssFunctionVendorRegexStr = cssFunctionVendorRegexStr;
+  validator.cssFunctionAnyRegexStr = cssFunctionAnyRegexStr;
+  validator.cssUnitAnyRegexStr = cssUnitAnyRegexStr;
+
+  return validator;
+})();