Shiny new property optimizer
authorTimur Kristóf <venemo@msn.com>
Thu, 27 Feb 2014 23:36:57 +0000 (00:36 +0100)
committerTimur Kristóf <venemo@msn.com>
Thu, 27 Feb 2014 23:36:57 +0000 (00:36 +0100)
lib/properties/compact.js [new file with mode: 0644]
lib/properties/optimizer.js
lib/selectors/optimizer.js
test/data/big-min.css
test/unit-test.js

diff --git a/lib/properties/compact.js b/lib/properties/compact.js
new file mode 100644 (file)
index 0000000..02f1809
--- /dev/null
@@ -0,0 +1,1290 @@
+
+// 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 760067f..0e3ed5e 100644 (file)
@@ -232,14 +232,23 @@ module.exports = function Optimizer(compatibility) {
     return flat.join(';');
   };
 
+  var compact = require('./compact');
+
   return {
-    process: function(body, allowAdjacent) {
+    process: function(body, allowAdjacent, skipCompacting) {
+      var result = body;
+
       var tokens = tokenize(body);
-      if (!tokens)
-        return body;
+      if (tokens) {
+        var optimized = optimize(tokens, allowAdjacent);
+        result = rebuild(optimized);
+      }
+
+      if (!skipCompacting) {
+        result = compact(result);
+      }
 
-      var optimized = optimize(tokens, allowAdjacent);
-      return rebuild(optimized);
+      return result;
     }
   };
 };
index d430afc..0838e96 100644 (file)
@@ -251,7 +251,7 @@ module.exports = function Optimizer(data, context, options) {
         joinsAt.push((joinsAt[j - 1] || 0) + bodies[j].split(';').length);
     }
 
-    var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt);
+    var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt, true);
     var optimizedProperties = optimizedBody.split(';');
 
     var processedCount = processedTokens.length;
index 7275eb5..8c094cd 100644 (file)
@@ -5,7 +5,7 @@ audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
 html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}
 button,html,input,select,textarea{font-family:sans-serif}
 body{margin:0}
-a:focus{outline:thin dotted}
+a:focus{outline:dotted thin}
 a:active,a:hover{outline:0}
 h1,h2,h3,h4,h5,h6{margin:0;font-weight:700}
 p{-webkit-margin-before:0;-webkit-margin-after:0}
@@ -24,7 +24,7 @@ sup{top:-.5em}
 sub{bottom:-.25em}
 ol,ul{margin:0;padding:0;list-style-type:none}
 dd{margin:0 0 0 40px}
-nav ol,nav ul{list-style:none;list-style-image:none}
+nav ol,nav ul{list-style:none}
 img{-ms-interpolation-mode:bicubic}
 svg:not(:root){overflow:hidden}
 figure,form{margin:0}
@@ -260,7 +260,7 @@ img[width="202"]{margin-bottom:4px}
 .btn.disabled:hover,input[type=submit].disabled{background-image:none;background-color:#e6e6e6;cursor:default}
 .btn_fonce.active,.btn_fonce.disabled,.btn_fonce:active,.btn_fonce:hover,.btn_fonce[disabled]{color:#fff;background-color:#16212c}
 .btn_abo.active,.btn_abo.disabled,.btn_abo:active,.btn_abo:hover,.btn_abo[disabled]{color:#2e3942;background-color:#ffc600}
-.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}
+.btn:focus{outline:#333 dotted thin;outline:-webkit-focus-ring-color 5px;outline-offset:-2px}
 .btn:active,.btn_abo:active,.btn_fonce:active,.btn_petit:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);outline:0}
 .btn:active,.btn_petit:active{background-color:#e6e6e6}
 .btn_fonce:active{background-color:#000b15}
@@ -334,7 +334,7 @@ input[type=submit].btn_petit{*padding-top:3px;*padding-bottom:3px}
 .filet_plus .bg_plus{background:#b9c0c5;padding:0 5px}
 .filet_plus .plus{color:#fff}
 .pic_debrief_abo{display:inline-block;margin:0 8px 0 0;vertical-align:text-bottom;width:24px;height:24px;background:url(/medias/web/img/sprites/pictos_abos.png) no-repeat 0 -24px}
-.pic_commentes_abo{display:inline-block;margin:0 8px 0 0;vertical-align:text-bottom;height:23px;width:32px;background:url(/medias/web/img/sprites/pictos_abos.png) no-repeat 0 0}
+.pic_commentes_abo{display:inline-block;margin:0 8px 0 0;vertical-align:text-bottom;height:23px;width:32px;background:url(/medias/web/img/sprites/pictos_abos.png) no-repeat}
 .liste_bordure li{padding:8px 16px 6px;border-bottom:1px solid #eef1f5}
 .liste_chevron{display:block;padding:0 0 0 10px;position:relative}
 .chevron{display:inline-block}
@@ -658,7 +658,7 @@ img[height="97"]+.ico29x29{bottom:6%;left:3.5%}
 #footer_services .liste_tv .logo+p{width:230px;display:inline-block;line-height:14px;font-weight:700}
 #footer_services .liste_tv .logo+p span{font-size:10px}
 #footer_services .liste_tv .logo+p b{display:block;font-size:11px}
-#footer_services .liste_tv .logo,#footer_services .liste_tv .note{display:inline-block;margin:0 5px 0 0;width:47px;height:27px;background:url(/medias/web/img/sprites/tv.png) no-repeat 0 0;text-indent:-9999px;vertical-align:baseline}
+#footer_services .liste_tv .logo,#footer_services .liste_tv .note{display:inline-block;margin:0 5px 0 0;width:47px;height:27px;background:url(/medias/web/img/sprites/tv.png) no-repeat;text-indent:-9999px;vertical-align:baseline}
 #footer_services .liste_tv .note{float:left;margin-top:2px}
 #footer_services .liste_tv .logo_france_2{background-position:0 -28px}
 #footer_services .liste_tv .logo_france_3{background-position:0 -56px}
@@ -710,7 +710,7 @@ img[height="97"]+.ico29x29{bottom:6%;left:3.5%}
 #footer .copy a{color:#464f57}
 #footer .description{color:#a2a9ae;padding:3px 13px;line-height:120%}
 #header_facebook,#header_google,#header_twitter{position:relative}
-.conteneur_popinbox{position:absolute;z-index:10;top:20px;left:-145px;padding:11px 0 0;-webkit-box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);-moz-box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);background:transparent url(/medias/web/img/habillage/lightbox_sociaux_coche.png) no-repeat center top;display:none}
+.conteneur_popinbox{position:absolute;z-index:10;top:20px;left:-145px;padding:11px 0 0;-webkit-box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);-moz-box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);box-shadow:-1px 4px 3px -2px rgba(0,11,21,.5);background:url(/medias/web/img/habillage/lightbox_sociaux_coche.png) no-repeat center top;display:none}
 .popinbox{padding:10px;background:#fff;overflow:visible}
 .sociaux .popinbox{width:292px;text-indent:0}
 #header_facebook_contenu{position:relative;height:258px}
@@ -759,7 +759,7 @@ img[height="97"]+.ico29x29{bottom:6%;left:3.5%}
 #header{font-size:12px;text-align:left}
 #header a{display:inline-block}
 .conteneur_haut{width:1000px;margin:0 auto}
-#surheader,#surheader .conteneur_haut{background:#000b15;background:#1e5799;background:-moz-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#1e5799),color-stop(0%,#2d3841),color-stop(100%,#010c16));background:-webkit-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-o-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-ms-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2d3841', endColorstr='#010c16', GradientType=0);height:25px;line-height:25px}
+#surheader,#surheader .conteneur_haut{background:#1e5799;background:-moz-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#1e5799),color-stop(0%,#2d3841),color-stop(100%,#010c16));background:-webkit-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-o-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-ms-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2d3841', endColorstr='#010c16', GradientType=0);height:25px;line-height:25px}
 #surheader .droit{width:400px;float:right}
 #surheader .gauche{width:600px;float:left}
 #surheader a,#surheader span{color:#fff;font-size:11px}
@@ -902,7 +902,7 @@ label i{font-style:normal;display:none}
 #ariane_az .obf,#ariane_az a{padding:0 9px;color:#000;white-space:nowrap}
 #ariane_az .suite_entrees{background:#f8f9fb;position:absolute;left:-9999px}
 #ariane_az .suite_entrees p{border-bottom:1px solid #eef1f5;line-height:33px;font-size:12px;font-weight:700}
-#nav_ariane{height:35px;overflow:hidden;background:url(/medias/web/img/sprites/sous_nav.png) repeat left -630px}
+#nav_ariane{height:35px;overflow:hidden;background:url(/medias/web/img/sprites/sous_nav.png) left -630px}
 #nav_ariane ul{float:left;width:950px;overflow:hidden}
 #nav_ariane li{display:block;float:left}
 #nav_ariane a,#nav_ariane h1 span.obf{display:inline-block;height:23px;padding:12px 8px 0;font-size:1.2rem;line-height:100%;font-weight:700;white-space:nowrap}
@@ -1040,7 +1040,7 @@ label i{font-style:normal;display:none}
 .barre_outils .outil{float:left;padding:2px 6px 0;height:25px;color:#747B83}
 .barre_outils .partage{float:right;height:26px;margin:0;padding-left:10px;border-left:1px solid #eef1f5;color:#747b83}
 .barre_outils .partage+span{height:26px;vertical-align:middle}
-.barre_outils .reagir span{width:12px;height:11px;background:url(/medias/web/img/sprites/icos_petites.png) no-repeat 0 0;vertical-align:middle}
+.barre_outils .reagir span{width:12px;height:11px;background:url(/medias/web/img/sprites/icos_petites.png) no-repeat;vertical-align:middle}
 .barre_outils .classer span{width:11px;height:11px;background:url(/medias/web/img/sprites/icos_petites.png) no-repeat 0 -12px;vertical-align:baseline}
 .barre_outils .classer.actif span{background-position:-13px -12px}
 .barre_outils .imprimer span{width:12px;height:12px;background:url(/medias/web/img/sprites/icos_petites.png) no-repeat 0 -25px;vertical-align:baseline}
@@ -1534,7 +1534,7 @@ label.comparer input{margin-right:8px}
 .lien_img314x64.explorer_discours_2012{background:url(/medias/web/img/textes/elections/widget_explorer_discours_2012.png);text-indent:-9999px}
 *{margin:0;padding:0}
 form,img{border:0}
-ul{list-style-image:none;list-style-position:inside;list-style-type:none}
+ul{list-style:none inside}
 table{border-collapse:collapse}
 #mainContent{background:#fff;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:12px;color:#222}
 body>img{position:absolute}
@@ -1723,7 +1723,7 @@ body.iframe{padding-top:0}
 .site-liberation .hot-topics li{display:block;float:left;padding:3px 7px 5px;margin:3px 10px 3px 0}
 #header-liberation .header-base{border-top:1px solid #e0e0e0}
 #header-liberation .header-base .digitalpaper,#header-liberation .header-base .home,#header-liberation .header-base .links,#header-liberation .header-base .sites-info-search{height:120px}
-#header-liberation .header-base .home .logo{background:url(http://s0.libe.com/libe/img/common/logo-liberation-150.png?f613aa3caae2) no-repeat 0 0;width:150px;height:55px;margin-top:33px}
+#header-liberation .header-base .home .logo{background:url(http://s0.libe.com/libe/img/common/logo-liberation-150.png?f613aa3caae2) no-repeat;width:150px;height:55px;margin-top:33px}
 #header-liberation .header-base .links{display:block;width:280px;height:110px;padding-top:10px}
 #header-liberation .header-base .links .lnk1,#header-liberation .header-base .links .lnk2{float:left}
 #header-liberation .header-base .links .lnk1{position:relative;width:123px}
@@ -1792,18 +1792,18 @@ body.iframe{padding-top:0}
 #core-liberation .text sub,#core-liberation .text sup{line-height:10px}
 #core-liberation .text-static h2,#core-liberation .text-static h3,#core-liberation .text-static h4{font-family:Verdana,sans-serif}
 #core-liberation .text-static h4{font-size:16px;font-weight:700;margin-bottom:15px}
-#core-liberation .text-static ul li{margin-bottom:10px;list-style:disc inside none}
+#core-liberation .text-static ul li{margin-bottom:10px;list-style:disc inside}
 #core-liberation .text-https h2,#core-liberation .text-https h3,#core-liberation .text-https h4{font-family:Verdana,sans-serif;font-weight:400}
 #core-liberation .text-https h4{font-size:16px;font-weight:700;margin-bottom:15px}
 #core-liberation .text-https p{padding:0}
-#core-liberation .text-https ul li{margin-bottom:10px;list-style:disc inside none}
+#core-liberation .text-https ul li{margin-bottom:10px;list-style:disc inside}
 #core-liberation .text-benefits h3{font-weight:400;font-size:17px}
 #core-liberation .text-404 h3{font-style:italic;font-size:16px}
 #core-liberation .text-404 p{font-family:Georgia,"Times New Roman",Times,serif;font-style:italic;font-size:13px}
 form input[type=password],form input[type=text]{border:1px solid;padding:3px;height:14px}
 form select{padding:1px;height:20px}
 #core-liberation form textarea{resize:vertical}
-#core-liberation form checkbox,#core-liberation form input[type=file]:focus,#core-liberation form input[type=password]:focus,#core-liberation form input[type=text]:focus,#core-liberation form select,#core-liberation form textarea:focus{outline:1px solid}
+#core-liberation form checkbox,#core-liberation form input[type=file]:focus,#core-liberation form input[type=password]:focus,#core-liberation form input[type=text]:focus,#core-liberation form select,#core-liberation form textarea:focus{outline:solid 1px}
 #core-liberation .error_ajax,#core-liberation .error_ajax_form{background:#ddd;border:1px solid #9d9d9d;padding:10px 10px 12px}
 #core-liberation .error_ajax_form{position:absolute;top:130px;left:105px;width:230px}
 #core-liberation .new_comment_form_wrapper{position:relative}
@@ -2004,7 +2004,7 @@ body.auth-unlogged #core-liberation .form-monlibe-unlogged form{opacity:.3;-ms-f
 #core-liberation .block-comments .block-content .comment_libe>.comment_outer .meta .icon{position:absolute;right:0;top:0;display:block;width:36px;height:13px;background:url(http://s0.libe.com/libe/img/common/_sprites_icons/icons.png?9914d0d70a49) no-repeat 0 -84px}
 #core-liberation .block-comments .block-content .comment_libe>.comment_outer .meta .details,#core-liberation .block-comments .block-content .comment_libe>.comment_outer .meta .note,#core-liberation .block-comments .block-content .comment_libe>.comment_outer .meta .who{padding-right:41px}
 #core-liberation .block-comments .block-content .is_removed>.comment_outer{padding:3px 8px 5px}
-#core-liberation .block-comments .block-content .is_removed>.comment_outer .icon{float:left;display:block;width:12px;height:11px;margin:3px 8px 0 0;background:url(http://s0.libe.com/libe/img/common/_sprites_icons/icons.png?9914d0d70a49) no-repeat 0 0}
+#core-liberation .block-comments .block-content .is_removed>.comment_outer .icon{float:left;display:block;width:12px;height:11px;margin:3px 8px 0 0;background:url(http://s0.libe.com/libe/img/common/_sprites_icons/icons.png?9914d0d70a49) no-repeat}
 #core-liberation .block-comments .comment_replies{padding:10px;display:none;border-bottom:1px solid}
 #core-liberation .block-comments .comment_cutoff,#core-liberation .block-usercomments .comment_replies{display:block}
 #core-liberation .block-usercomments .noreplies{display:none}
@@ -2231,7 +2231,7 @@ a.god:hover{background:#3c3c3c;color:#fff;text-decoration:none}
 #core-liberation .cartridge .btn-back,#core-liberation .cartridge .btn-comment{text-align:center}
 #core-liberation .cartridge .btn-back span,#core-liberation .cartridge .btn-comment span{display:block;padding:6px 7px 0;font-weight:700}
 #core-liberation .cartridge a.btn-back:hover,#core-liberation .cartridge a.btn-comment:hover{text-decoration:none}
-#core-liberation .cartridge a.btn-comment-disabled{background:transparent url(http://s0.libe.com/libe/img/common/bg-btn-comment.png?593ec6d1f747)}
+#core-liberation .cartridge a.btn-comment-disabled{background:url(http://s0.libe.com/libe/img/common/bg-btn-comment.png?593ec6d1f747)}
 #core-liberation .cartridge a.btn-comment-disabled:hover{cursor:default}
 #core-liberation .cartridge .options-tab{position:relative}
 #core-liberation .cartridge .options-tab-content{display:none;position:absolute;padding:5px 9px 8px;border:1px solid;width:180px;text-align:right;right:8px;font-size:11px;z-index:100}
@@ -2333,8 +2333,8 @@ body.init-bar-is-closed #bar-liberation{height:15px}
 #bar-liberation .content .activities-stream,#bar-liberation .content .close,#bar-liberation .content .login,#bar-liberation .content .mail-box,#bar-liberation .content .open,#bar-liberation .content .other,#bar-liberation .content .personal-options{display:none;border-left:1px solid;border-right:1px solid;border-bottom:1px solid;position:absolute;top:0;height:40px}
 #bar-liberation .content .activities-stream .list .more{display:none}
 #bar-liberation .content .activities-stream .list .text,#bar-liberation .content .activities-stream .list p{display:inline}
-#bar-liberation .content a.displayer .arrow{background:url(http://s0.libe.com/libe/img/common/_sprites_header/triangle_ferme.png?1ecaa0c231c9) no-repeat 0 0;display:block;position:absolute;right:10px;top:16px;width:10px;height:10px}
-#bar-liberation .content a.displayer:hover .arrow{background:url(http://s0.libe.com/libe/img/common/_sprites_header/triangle_ferme_grey.png?a9a52344ba82) no-repeat 0 0}
+#bar-liberation .content a.displayer .arrow{background:url(http://s0.libe.com/libe/img/common/_sprites_header/triangle_ferme.png?1ecaa0c231c9) no-repeat;display:block;position:absolute;right:10px;top:16px;width:10px;height:10px}
+#bar-liberation .content a.displayer:hover .arrow{background:url(http://s0.libe.com/libe/img/common/_sprites_header/triangle_ferme_grey.png?a9a52344ba82) no-repeat}
 #bar-liberation .content a.displayer .arrow-displayed,#bar-liberation .content a.displayer:hover .arrow-displayed{background:url(http://s0.libe.com/libe/img/common/_sprites_header/triangle_ouvert.png?c782eb482038) no-repeat 1px 1px}
 #bar-liberation .content ul.list li{margin:0 10px;min-height:32px;padding:6px 0 2px;border-bottom:1px solid;line-height:16px}
 #bar-liberation .content ul.list li a,#bar-liberation .content ul.list li a:hover,#core-liberation .block-activities .block-content ul li a,#core-liberation .block-activities .block-content ul li a:hover{text-decoration:underline}
@@ -2477,8 +2477,8 @@ body.access-ess #page-paywall .content .arguments .arg{float:none;margin:auto}
 .site-liberation .block-call-items .mini-tpl:last-of-type{border-bottom:0;margin-bottom:0:}
 .site-liberation .block-call-items .mini-tpl .right{float:right}
 .site-liberation .block-call-items .mini-tpl .lnk-libeplus,.site-liberation .block-call-items .mini-tpl .lnk-libeplus-big{background:0 0;padding:0}
-.site-liberation .block-call-items .mini-tpl h2.lnk-libeplus:after,.site-liberation .block-call-items .mini-tpl h3.lnk-libeplus:after{content:'';display:inline-block;width:56px;height:14px;margin-left:10px;background:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus-big2.png?d4d49ea9ef21) no-repeat 0 0}
-.site-liberation .block-call-items .mini-tpl .list-linked-items a.lnk-libeplus:after,.site-liberation .block-call-items .mini-tpl h5.lnk-libeplus:after{content:'';display:inline-block;width:56px;height:10px;margin-left:5px;background:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus2.png?39fe048f4782) no-repeat 0 0}
+.site-liberation .block-call-items .mini-tpl h2.lnk-libeplus:after,.site-liberation .block-call-items .mini-tpl h3.lnk-libeplus:after{content:'';display:inline-block;width:56px;height:14px;margin-left:10px;background:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus-big2.png?d4d49ea9ef21) no-repeat}
+.site-liberation .block-call-items .mini-tpl .list-linked-items a.lnk-libeplus:after,.site-liberation .block-call-items .mini-tpl h5.lnk-libeplus:after{content:'';display:inline-block;width:56px;height:10px;margin-left:5px;background:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus2.png?39fe048f4782) no-repeat}
 * html .site-liberation .block-call-items .mini-tpl h2.lnk-libeplus,* html .site-liberation .block-call-items .mini-tpl h3.lnk-libeplus{background-image:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus-big2.png?d4d49ea9ef21);background-repeat:no-repeat;background-position:right 4px;padding-right:60px}
 * html .site-liberation .block-call-items .mini-tpl .list-linked-items a.lnk-libeplus,* html .site-liberation .block-call-items .mini-tpl h5.lnk-libeplus{background-image:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus2.png?39fe048f4782);background-repeat:no-repeat;background-position:right 4px;padding-right:60px}
 :first-child+html .site-liberation .block-call-items .mini-tpl h2.lnk-libeplus,:first-child+html .site-liberation .block-call-items .mini-tpl h3.lnk-libeplus{background-image:url(http://s0.libe.com/libe/img/common/ico-lnk-libeplus-big2.png?d4d49ea9ef21);background-repeat:no-repeat;background-position:right 4px;padding-right:60px}
@@ -2508,7 +2508,7 @@ body.access-ess #page-paywall .content .arguments .arg{float:none;margin:auto}
 .site-liberation .block-call-items .mini-tpl .whosaid h5 .theme{font-size:14px}
 .site-liberation .block-call-items .mini-tpl .whosaid h5 a.theme:hover{text-decoration:underline}
 .site-liberation .block-call-items .mini-tpl .whosaid h3{font-size:26px;font-weight:400;margin-bottom:28px}
-.site-liberation .block-call-items .mini-tpl .whosaid a.zap{display:block;position:absolute;width:78px;height:21px;background:url(http://s0.libe.com/libe/img/common/btn_shaker.gif?6340e450364b) no-repeat 0 0;bottom:14px;right:14px}
+.site-liberation .block-call-items .mini-tpl .whosaid a.zap{display:block;position:absolute;width:78px;height:21px;background:url(http://s0.libe.com/libe/img/common/btn_shaker.gif?6340e450364b) no-repeat;bottom:14px;right:14px}
 .site-liberation .block-call-items .mini-tpl .whosaid .answer{margin-top:10px}
 .site-liberation .block-call-items .mini-tpl .whosaid .answer h4{margin-bottom:10px}
 .site-liberation .block-call-items .mini-tpl .whosaid .answer a{float:right;font-size:14px}
index 473a303..86ee3c9 100644 (file)
@@ -142,7 +142,7 @@ vows.describe('clean-units').addBatch({
     ],
     'not inside calc method #2': [
       'div{margin:-moz-calc(50% + 15px) -moz-calc(50% + 15px);margin:calc(50% + .5rem) calc(50% + .5rem)}',
-      'div{margin:-moz-calc(50% + 15px) -moz-calc(50% + 15px);margin:calc(50% + .5rem) calc(50% + .5rem)}'
+      'div{margin:-moz-calc(50% + 15px);margin:calc(50% + .5rem)}'
     ],
     'not inside calc method with more parentheses': [
       'div{height:-moz-calc((10% + 12px)/2 + 10em)}',
@@ -366,7 +366,7 @@ vows.describe('clean-units').addBatch({
     'border\'s none to none': 'a{border:none}p{border-top:none}',
     'background:transparent to zero': [
       'a{background:transparent}p{background:transparent url(logo.png)}',
-      'a{background:0 0}p{background:transparent url(logo.png)}'
+      'a{background:0 0}p{background:url(logo.png)}'
     ],
     'outline:none to outline:0': [
       'a{outline:none}',
@@ -1566,6 +1566,226 @@ title']{display:block}",
     'transition-property': ['transition'],
     'transition-timing-function': ['transition']
   }, { vendorPrefixes: ['animation', 'transition'] }),
+  'redefined more granular properties with property merging': cssContext({
+    'should merge background with background-attachment': [
+      'a{background:0;background-attachment:fixed}',
+      'a{background:0 fixed}'
+    ],
+    'should NOT merge background with inherited background-attachment': [
+      'a{background:0;background-attachment:inherit}',
+      'a{background:0;background-attachment:inherit}'
+    ],
+    'should merge background with background-color': [
+      'a{background:0;background-color:#9fce00}',
+      'a{background:#9fce00 0}'
+    ],
+    'should NOT merge background with inherited background-color': [
+      'a{background:0;background-color:inherit}',
+      'a{background:0;background-color:inherit}'
+    ],
+    'should merge background with background-image': [
+      'a{background:0;background-image:url(hello_world)}',
+      'a{background:url(hello_world) 0}'
+    ],
+    'should NOT merge background with inherited background-image': [
+      'a{background:0;background-image:inherit}',
+      'a{background:0;background-image:inherit}'
+    ],
+    'should merge background with background-position': [
+      'a{background:0;background-position:3px 4px}',
+      'a{background:3px 4px}'
+    ],
+    'should NOT merge background with inherited background-position': [
+      'a{background:0;background-position:inherit}',
+      'a{background:0;background-position:inherit}',
+    ],
+    'should merge background with background-repeat': [
+      'a{background:0;background-repeat:repeat-y}',
+      'a{background:repeat-y 0}'
+    ],
+    'should NOT merge background with inherited background-repeat': [
+      'a{background:0;background-repeat:inherit}',
+      'a{background:0;background-repeat:inherit}',
+    ],
+    'should merge outline with outline-color': [
+      'a{outline:1px;outline-color:#9fce00}',
+      'a{outline:#9fce00 1px}'
+    ],
+    'should NOT merge outline with inherited outline-color': [
+      'a{outline:0;outline-color:inherit}',
+      'a{outline:0;outline-color:inherit}'
+    ],
+    'should merge outline with outline-style': [
+      'a{outline:0;outline-style:dashed}',
+      'a{outline:dashed 0}'
+    ],
+    'should NOT merge outline with inherited outline-style': [
+      'a{outline:0;outline-style:inherit}',
+      'a{outline:0;outline-style:inherit}'
+    ],
+    'should merge outline with outline-width': [
+      'a{outline:0;outline-width:5px}',
+      'a{outline:5px}'
+    ],
+    'should NOT merge outline with inherited outline-width': [
+      'a{outline:0;outline-width:inherit}',
+      'a{outline:0;outline-width:inherit}'
+    ]
+  }),
+  'shorthand properties': cssContext({
+    'shorthand background' : [
+      'div{background-color:#111;background-image:url(aaa);background-repeat:repeat;background-position:0 0;background-attachment:scroll}',
+      'div{background:#111 url(aaa)}'
+    ],
+    'shorthand important background' : [
+      'div{background-color:#111!important;background-image:url(aaa)!important;background-repeat:repeat!important;background-position:0 0!important;background-attachment:scroll!important}',
+      'div{background:#111 url(aaa)!important}'
+    ]
+  }),
+  'care about understandability of shorthand components': cssContext({
+    'linear-gradient should NOT clear out background with color only' : [
+      'div{background:#fff;background:linear-gradient(whatever)}',
+      'div{background:#fff;background:linear-gradient(whatever)}'
+    ],
+    'linear-gradient should NOT clear out background with color only, even if it has a color' : [
+      'div{background:#fff;background:#222 linear-gradient(whatever)}',
+      'div{background:#fff;background:#222 linear-gradient(whatever)}'
+    ],
+    'a background-image with just a linear-gradient should not be compacted to a shorthand' : [
+      'div{background-color:#111;background-image:linear-gradient(aaa);background-repeat:no-repeat;background-position:0 0;background-attachment:scroll}',
+      'div{background-color:#111;background-image:linear-gradient(aaa);background-repeat:no-repeat;background-position:0 0;background-attachment:scroll}'
+    ],
+    'a background-image with a none and a linear-gradient should result in two shorthands' : [
+      'div{background-color:#111;background-image:none;background-image:linear-gradient(aaa);background-repeat:repeat;background-position:0 0;background-attachment:scroll}',
+      'div{background:#111;background:#111 linear-gradient(aaa)}'
+    ],
+  }),
+  'merge same properties sensibly': cssContext({
+    'should merge color values with same understandability #1': [
+      'p{color:red;color:#fff;color:blue}',
+      'p{color:#00f}'
+    ],
+    'should merge color values with same understandability #2': [
+      'p{color:red;color:#fff;color:blue;color:transparent}',
+      'p{color:transparent}'
+    ],
+    'should NOT destroy less understandable values': [
+      'p{color:red;color:#fff;color:blue;color:rgba(1,2,3,.4)}',
+      'p{color:#00f;color:rgba(1,2,3,.4)}'
+    ],
+    'should destroy even less understandable values if a more understandable one comes after them': [
+      'p{color:red;color:#fff;color:blue;color:rgba(1,2,3,.4);color:#9fce00}',
+      'p{color:#9fce00}'
+    ],
+    'should merge functions with the same name but keep different functions intact': [
+      'p{background:-webkit-linear-gradient(aaa);background:-webkit-linear-gradient(bbb);background:linear-gradient(aaa);}',
+      'p{background:-webkit-linear-gradient(bbb);background:linear-gradient(aaa)}'
+    ],
+    'should merge nonimportant + important into one important': [
+      'a{color:#aaa;color:#bbb!important}',
+      'a{color:#bbb!important}'
+    ],
+    'should merge important + nonimportant into one important': [
+      'a{color:#aaa!important;color:#bbb}',
+      'a{color:#aaa!important}'
+    ],
+    'should merge importants just like nonimportants while also overriding them': [
+      'p{color:red!important;color:#fff!important;color:blue!important;color:rgba(1,2,3,.4)}',
+      'p{color:#00f!important}'
+    ]
+  }),
+  'shorthand granular properties when other granular properties are already covered by the shorthand': cssContext({
+    'should consider the already existing margin to shorthand margin-top and margin-bottom': [
+      'p{margin:5px;margin-top:foo(1);margin-left:foo(2)}',
+      'p{margin:5px;margin:foo(1) 5px 5px foo(2)}'
+    ],
+    'should merge margin-top and margin-left with shorthand if their understandability is the same': [
+      'p{margin:5px;margin-top:1px;margin-left:2px}',
+      'p{margin:1px 5px 5px 2px}'
+    ],
+    'should NOT shorthand to margin-top if the result would be longer than the input': [
+      'p{margin:5px;margin-top:foo(1)}',
+      'p{margin:5px;margin-top:foo(1)}'
+    ],
+    'should consider the already existing background to shorthand background-color': [
+      'p{background:#9fce00;background-color:rgba(1,2,3,.4)}',
+      'p{background:#9fce00;background:rgba(1,2,3,.4)}',
+    ],
+    'should NOT touch important outline-color but should minify default value of outline to 0': [
+      'p{outline:medium;outline-color:#9fce00!important}',
+      'p{outline:0;outline-color:#9fce00!important}',
+    ]
+  }),
+  'take advantage of importants for optimalization opportunities': cssContext({
+    'should take into account important margin-left to shorthand non-important margin-top, margin-right and margin-bottom': [
+      'p{margin-top:1px;margin-right:2px;margin-bottom:3px;margin-left:4px !important}',
+      'p{margin:1px 2px 3px;margin-left:4px!important}'
+    ],
+    'should take into account important margin-bottom and margin-left to shorten shorthanded non-important margin-top and margin-bottom': [
+      'p{margin-top:1px;margin-right:2px;margin-bottom:3px!important;margin-left:4px !important}',
+      'p{margin:1px 2px;margin-bottom:3px!important;margin-left:4px!important}'
+    ],
+    'should take into account important margin-right and margin-left to shorten shorthanded non-important margin-top and margin-bottom': [
+      'p{margin-top:1px;margin-bottom:3px;margin-right:2px!important;margin-left:4px !important}',
+      'p{margin:1px 0 3px;margin-right:2px!important;margin-left:4px!important}'
+    ],
+    'should take into account important margin-right and margin-left to shorten shorthanded non-important margin-top and margin-bottom #2': [
+      'p{margin-top:1px;margin-bottom:1px;margin-right:2px!important;margin-left:4px !important}',
+      'p{margin:1px;margin-right:2px!important;margin-left:4px!important}'
+    ],
+    'should take into account important background-color and shorthand others into background': [
+      'p{background-color:#9fce00!important;background-image:url(hello);background-attachment:scroll;background-position:1px 2px;background-repeat:repeat-y}',
+      'p{background-color:#9fce00!important;background:url(hello) repeat-y 1px 2px}'
+    ],
+    'should take into account important outline-color and default value of outline-width': [
+      'p{outline:inset medium;outline-color:#9fce00!important;outline-style:inset!important}',
+      'p{outline:0;outline-color:#9fce00!important;outline-style:inset!important}'
+    ],
+    'should take into account important background-position remove its irrelevant counterpart': [
+      'p{background:#9fce00 url(hello) 4px 5px;background-position:5px 3px!important}',
+      'p{background:#9fce00 url(hello);background-position:5px 3px!important}',
+    ],
+    'should take into account important background-position and assign the shortest possible value for its irrelevant counterpart': [
+      'p{background:transparent;background-position:5px 3px!important}',
+      'p{background:0;background-position:5px 3px!important}',
+    ]
+  }),
+  'properly care about inherit': cssContext({
+    'merge multiple inherited margin granular properties into one inherited shorthand': [
+      'p{margin-top:inherit;margin-right:inherit;margin-bottom:inherit;margin-left:inherit}',
+      'p{margin:inherit}'
+    ],
+    'merge multiple inherited background granular properties into one inherited shorthand': [
+      'p{background-color:inherit;background-image:inherit;background-attachment:inherit;background-position:inherit;background-repeat:inherit}',
+      'p{background:inherit}'
+    ],
+    'when shorter, optimize inherited/non-inherited background granular properties into an inherited shorthand and some non-inherited granular properties': [
+      'p{background-color:inherit;background-image:inherit;background-attachment:inherit;background-position:inherit;background-repeat:repeat-y}',
+      'p{background:inherit;background-repeat:repeat-y}'
+    ],
+    'when shorter, optimize inherited/non-inherited background granular properties into a non-inherited shorthand and some inherited granular properties': [
+      'p{background-color:#9fce00;background-image:inherit;background-attachment:scroll;background-position:1px 2px;background-repeat:repeat-y}',
+      'p{background:#9fce00 repeat-y 1px 2px;background-image:inherit}'
+    ],
+    'put inherit to the place where it consumes the least space': [
+      'div{padding:0;padding-bottom:inherit;padding-right:inherit}',
+      'div{padding:inherit;padding-top:0;padding-left:0}'
+    ]
+  }),
+  'remove defaults from shorthands': cssContext({
+    'all-default background should be changed to shortest possible default value': [
+      'div{background:transparent none repeat 0 0 scroll}',
+      'div{background:0 0}'
+    ],
+    'default background components should be removed #1': [
+      'body{background:#9fce00 none repeat scroll}',
+      'body{background:#9fce00}'
+    ],
+    'default background components should be removed #2': [
+      'body{background:transparent none 1px 5px scroll}',
+      'body{background:1px 5px}'
+    ]
+  }),
   'complex granular properties': cssContext({
     'two granular properties': 'a{border-bottom:1px solid red;border-color:red}',
     'two same granular properties': 'a{border-color:rgba(0,0,0,.5);border-color:red}',